C# Language
Возможности C # 6.0
Поиск…
Вступление
Эта шестнадцатая итерация языка C # предоставляется компилятором Roslyn. Этот компилятор вышел с версией 4.6 .NET Framework, однако он может генерировать код с обратной совместимостью, чтобы разрешить таргетинг на ранние версии рамок. Код C # версии 6 может быть скомпилирован полностью совместимым образом с .NET 4.0. Его также можно использовать для ранних фреймворков, однако некоторые функции, требующие дополнительной поддержки фреймворка, могут работать некорректно.
замечания
Шестая версия C # была выпущена в июле 2015 года вместе с Visual Studio 2015 и .NET 4.6.
Помимо добавления некоторых новых функций языка, он включает полную переработку компилятора. Ранее csc.exe
было родным приложением Win32, написанным на C ++, а C # 6 теперь является управляемым .NET-программным обеспечением, написанным на C #. Этот переписчик был известен как проект «Roslyn», и код теперь открыт с открытым исходным кодом и доступен на GitHub .
Имя оператора
Оператор nameof
возвращает имя элемента кода в виде string
. Это полезно при бросании исключений, связанных с аргументами метода, а также при внедрении INotifyPropertyChanged
.
public string SayHello(string greeted)
{
if (greeted == null)
throw new ArgumentNullException(nameof(greeted));
Console.WriteLine("Hello, " + greeted);
}
Оператор nameof
вычисляется во время компиляции и изменяет выражение в строковый литерал. Это также полезно для строк, названных в честь их члена, который их раскрывает. Рассмотрим следующее:
public static class Strings
{
public const string Foo = nameof(Foo); // Rather than Foo = "Foo"
public const string Bar = nameof(Bar); // Rather than Bar = "Bar"
}
Поскольку выражения nameof
являются константами времени компиляции, их можно использовать в атрибутах, метках case
, операторах switch
и т. Д.
Удобно использовать nameof
с Enum
s. Вместо:
Console.WriteLine(Enum.One.ToString());
можно использовать:
Console.WriteLine(nameof(Enum.One))
В обоих случаях выход будет One
.
Оператор nameof
может обращаться к nameof
элементам, используя синтаксис типа static. Вместо того, чтобы делать:
string foo = "Foo";
string lengthName = nameof(foo.Length);
Может быть заменено на:
string lengthName = nameof(string.Length);
В обоих примерах выход будет Length
. Однако последний препятствует созданию ненужных экземпляров.
Хотя nameof
оператора работает с большинством языковых конструкций, существуют некоторые ограничения. Например, вы не можете использовать оператор nameof
для открытых общих типов или возвращаемых значений метода:
public static int Main()
{
Console.WriteLine(nameof(List<>)); // Compile-time error
Console.WriteLine(nameof(Main())); // Compile-time error
}
Кроме того, если вы примените его к родовому типу, параметр родового типа будет проигнорирован:
Console.WriteLine(nameof(List<int>)); // "List"
Console.WriteLine(nameof(List<bool>)); // "List"
Дополнительные примеры см. В этом разделе, посвященном nameof
.
Обходной путь для предыдущих версий ( подробнее )
Хотя оператор nameof
не существует в C # для версий до 6.0, аналогичную функциональность можно использовать с помощью MemberExpression
как MemberExpression
ниже:
Выражение:
public static string NameOf<T>(Expression<Func<T>> propExp)
{
var memberExpression = propExp.Body as MemberExpression;
return memberExpression != null ? memberExpression.Member.Name : null;
}
public static string NameOf<TObj, T>(Expression<Func<TObj, T>> propExp)
{
var memberExpression = propExp.Body as MemberExpression;
return memberExpression != null ? memberExpression.Member.Name : null;
}
Использование:
string variableName = NameOf(() => variable);
string propertyName = NameOf((Foo o) => o.Bar);
Обратите внимание, что этот подход заставляет nameof
дерево выражений при каждом вызове, поэтому производительность намного хуже по сравнению с nameof
оператора, который оценивается во время компиляции и имеет нулевые служебные данные во время выполнения.
Элементы функции выражения
Элементы функции, выражающие выражение, позволяют использовать лямбда-выражения в качестве элементов-членов. Для простых членов это может привести к более чистым и понятным кодам.
Функциональные выражения могут использоваться для свойств, индексаторов, методов и операторов.
свойства
public decimal TotalPrice => BasePrice + Taxes;
Эквивалентно:
public decimal TotalPrice
{
get
{
return BasePrice + Taxes;
}
}
Когда функция expression-bodied используется с свойством, свойство реализуется как свойство только для getter.
индексаторы
public object this[string key] => dictionary[key];
Эквивалентно:
public object this[string key]
{
get
{
return dictionary[key];
}
}
методы
static int Multiply(int a, int b) => a * b;
Эквивалентно:
static int Multiply(int a, int b)
{
return a * b;
}
Который также может использоваться с void
методами:
public void Dispose() => resource?.Dispose();
В класс Pair<T>
можно добавить переопределение ToString
:
public override string ToString() => $"{First}, {Second}";
Кроме того, этот упрощенный подход работает с ключевым словом override
:
public class Foo
{
public int Bar { get; }
public string override ToString() => $"Bar: {Bar}";
}
операторы
Это также может использоваться операторами:
public class Land
{
public double Area { get; set; }
public static Land operator +(Land first, Land second) =>
new Land { Area = first.Area + second.Area };
}
Ограничения
Элементы функции с выражением имеют некоторые ограничения. Они не могут содержать операторы блоков и любые другие операторы, которые содержат блоки: if
, switch
, for
, foreach
, while
, do
, try
и т. Д.
Некоторые операторы if
могут быть заменены на тернарные операторы. Некоторые операторы for
и foreach
могут быть преобразованы в запросы LINQ, например:
IEnumerable<string> Digits
{
get
{
for (int i = 0; i < 10; i++)
yield return i.ToString();
}
}
IEnumerable<string> Digits => Enumerable.Range(0, 10).Select(i => i.ToString());
Во всех остальных случаях можно использовать старый синтаксис для членов функции.
Элементы функции Expression-body могут содержать async
/ await
, но это часто избыточно:
async Task<int> Foo() => await Bar();
Может быть заменено на:
Task<int> Foo() => Bar();
Исключительные фильтры
Фильтры исключений дают разработчикам возможность добавить условие (в виде boolean
выражения) в блок catch , позволяя catch
выполнять, только если условие оценивается как true
.
Фильтры исключений позволяют распространять информацию об отладке в исходном исключении, где при использовании оператора if
внутри блока catch
и повторного выброса исключения останавливает распространение отладочной информации в исходном исключении. При использовании фильтров исключений исключение продолжает распространяться вверх в стеке вызовов, если условие не выполняется. В результате фильтры исключений значительно облегчают процесс отладки. Вместо того, чтобы останавливаться на инструкции throw
, отладчик остановится на выражении, исключающем исключение, с сохранением текущего состояния и всех локальных переменных. Аналогичным образом затрагиваются аварийные свалки.
Фильтры исключений поддерживались CLR с самого начала, и они были доступны из VB.NET и F # в течение более десяти лет, подвергая часть модели обработки исключений CLR. Только после выпуска C # 6.0 функциональность также была доступна для разработчиков C #.
Использование фильтров исключений
Фильтры исключений используются путем добавления предложения when
в выражение catch
. Можно использовать любое выражение, возвращающее bool
в предложении when
(кроме ожидания ). Объявленная переменная exception ex
доступна из предложения when
:
var SqlErrorToIgnore = 123;
try
{
DoSQLOperations();
}
catch (SqlException ex) when (ex.Number != SqlErrorToIgnore)
{
throw new Exception("An error occurred accessing the database", ex);
}
Множество блоков catch
when
предложения могут быть объединены. Первое, when
предложение, возвращающее true
приведет к тому, что исключение будет обнаружено. Его блок catch
будет введен, в то время как другие предложения catch
будут проигнорированы (их, when
предложения не будут оцениваться). Например:
try
{ ... }
catch (Exception ex) when (someCondition) //If someCondition evaluates to true,
//the rest of the catches are ignored.
{ ... }
catch (NotImplementedException ex) when (someMethod()) //someMethod() will only run if
//someCondition evaluates to false
{ ... }
catch(Exception ex) // If both when clauses evaluate to false
{ ... }
Опасно, когда статья
предосторожность
Может быть опасно использовать фильтры исключений: когда
Exception
выбрано из предложенияwhen
,Exception
из предложенияwhen
игнорируется и считаетсяfalse
. Такой подход позволяет разработчикам писать,when
клаузулы, не заботясь о недопустимых случаях.
Следующий пример иллюстрирует такой сценарий:
public static void Main()
{
int a = 7;
int b = 0;
try
{
DoSomethingThatMightFail();
}
catch (Exception ex) when (a / b == 0)
{
// This block is never reached because a / b throws an ignored
// DivideByZeroException which is treated as false.
}
catch (Exception ex)
{
// This block is reached since the DivideByZeroException in the
// previous when clause is ignored.
}
}
public static void DoSomethingThatMightFail()
{
// This will always throw an ArgumentNullException.
Type.GetType(null);
}
Обратите внимание, что фильтры исключений исключают проблемы с запутанными номерами строк, связанные с использованием throw
когда код ошибки находится в одной и той же функции. Например, в этом случае номер строки отображается как 6 вместо 3:
1. int a = 0, b = 0;
2. try {
3. int c = a / b;
4. }
5. catch (DivideByZeroException) {
6. throw;
7. }
Номер строки исключений сообщается как 6, потому что ошибка была поймана и повторно выбрана с помощью оператора throw
в строке 6.
То же самое происходит с фильтрами исключений:
1. int a = 0, b = 0;
2. try {
3. int c = a / b;
4. }
5. catch (DivideByZeroException) when (a != 0) {
6. throw;
7. }
В этом примере a
равно 0, тогда предложение catch
игнорируется, но 3 указывается как номер строки. Это происходит потому, что они не разматывают стек . Более конкретно, исключение не попадает в строку 5, потому a
на самом деле оно равно 0
и, следовательно, нет возможности исключить исключение в строке 6, потому что строка 6 не выполняется.
Регистрация в качестве побочного эффекта
Вызовы метода в состоянии могут вызывать побочные эффекты, поэтому фильтры исключения могут использоваться для запуска кода на исключениях, не вылавливая их. Общим примером, который использует это, является метод Log
который всегда возвращает false
. Это позволяет отслеживать информацию журнала во время отладки без необходимости повторного выброса исключения.
Имейте в виду, что, хотя это, кажется, удобный способ ведения журнала, это может быть рискованным, особенно если используются сторонние протоколирующие сборки. Они могут генерировать исключения при входе в не очевидные ситуации, которые могут быть легко обнаружены (см. Рискованное,
when(...)
выше).
try
{
DoSomethingThatMightFail(s);
}
catch (Exception ex) when (Log(ex, "An error occurred"))
{
// This catch block will never be reached
}
// ...
static bool Log(Exception ex, string message, params object[] args)
{
Debug.Print(message, args);
return false;
}
Общий подход в предыдущих версиях C # заключался в регистрации и повторном выбросе исключения.
try
{
DoSomethingThatMightFail(s);
}
catch (Exception ex)
{
Log(ex, "An error occurred");
throw;
}
// ...
static void Log(Exception ex, string message, params object[] args)
{
Debug.Print(message, args);
}
finally
блок
Блок finally
выполняется каждый раз, независимо от того, выбрано ли исключение или нет. Одна тонкость с выражениями, when
есть фильтры исключений, выполняется дальше в стеке, прежде чем вводить внутренние блоки finally
. Это может привести к неожиданным результатам и поведению, когда код пытается изменить глобальное состояние (например, пользователь или культура текущего потока) и установить его обратно в блок finally
.
Пример: finally
блок
private static bool Flag = false;
static void Main(string[] args)
{
Console.WriteLine("Start");
try
{
SomeOperation();
}
catch (Exception) when (EvaluatesTo())
{
Console.WriteLine("Catch");
}
finally
{
Console.WriteLine("Outer Finally");
}
}
private static bool EvaluatesTo()
{
Console.WriteLine($"EvaluatesTo: {Flag}");
return true;
}
private static void SomeOperation()
{
try
{
Flag = true;
throw new Exception("Boom");
}
finally
{
Flag = false;
Console.WriteLine("Inner Finally");
}
}
Производительность:
Начните
EvaluatesTo: True
Внутренний Наконец
Ловить
Внешнее окончание
В приведенном выше примере, если метод SomeOperation
не желает «утечки», глобальное состояние изменяется на предложения вызывающего, when
оно должно быть, оно также должно содержать блок catch
для изменения состояния. Например:
private static void SomeOperation()
{
try
{
Flag = true;
throw new Exception("Boom");
}
catch
{
Flag = false;
throw;
}
finally
{
Flag = false;
Console.WriteLine("Inner Finally");
}
}
Также часто бывает, что IDisposable
вспомогательные классы используют семантику использования блоков для достижения той же цели, что и IDisposable.Dispose
всегда IDisposable.Dispose
до того, как исключение, вызванное внутри using
блока, начнет разворачивать стек.
Инициализаторы автоистории
Вступление
Свойства можно инициализировать с помощью оператора =
после закрытия }
. В приведенном ниже разделе « Coordinate
» показаны доступные параметры инициализации свойства:
public class Coordinate
{
public int X { get; set; } = 34; // get or set auto-property with initializer
public int Y { get; } = 89; // read-only auto-property with initializer
}
Аксессоры с различной видимостью
Вы можете инициализировать автоматические свойства, которые имеют различную видимость для их аксессуаров. Вот пример с защищенным сеттером:
public string Name { get; protected set; } = "Cheeze";
Аксессор также может быть internal
, internal protected
или private
.
Свойства только для чтения
Помимо гибкости с видимостью, вы также можете инициализировать автоматические свойства только для чтения. Вот пример:
public List<string> Ingredients { get; } =
new List<string> { "dough", "sauce", "cheese" };
В этом примере также показано, как инициализировать свойство сложным типом. Кроме того, авто-свойства не могут быть только для записи, поэтому также исключает инициализацию только для записи.
Старый стиль (pre C # 6.0)
До C # 6 это требовало гораздо более подробного кода. Мы использовали одну дополнительную переменную, называемую свойство backing для свойства, чтобы присвоить значение по умолчанию или инициализировать публичное свойство, как показано ниже,
public class Coordinate
{
private int _x = 34;
public int X { get { return _x; } set { _x = value; } }
private readonly int _y = 89;
public int Y { get { return _y; } }
private readonly int _z;
public int Z { get { return _z; } }
public Coordinate()
{
_z = 42;
}
}
Примечание. До C # 6.0 вы все равно можете инициализировать чтение и запись автоматически реализованных свойств (свойств с помощью getter и setter) внутри конструктора, но вы не можете инициализировать свойство inline с его объявлением
использование
Инициализаторы должны оценивать статические выражения, как и инициализаторы полей. Если вам нужно ссылаться на нестатические элементы, вы можете либо инициализировать свойства в конструкторах, как раньше, либо использовать свойства с выражением. Нестатические выражения, подобные приведенным ниже (закомментированные), генерируют ошибку компилятора:
// public decimal X { get; set; } = InitMe(); // generates compiler error
decimal InitMe() { return 4m; }
Но статические методы могут использоваться для инициализации автоматических свойств:
public class Rectangle
{
public double Length { get; set; } = 1;
public double Width { get; set; } = 1;
public double Area { get; set; } = CalculateArea(1, 1);
public static double CalculateArea(double length, double width)
{
return length * width;
}
}
Этот метод также можно применять к свойствам с различным уровнем доступа:
public short Type { get; private set; } = 15;
Инициализатор автоматического свойства позволяет присваивать свойства непосредственно в объявлении. Для свойств только для чтения он выполняет все требования, необходимые для обеспечения неизменности свойства. Рассмотрим, например, класс FingerPrint
в следующем примере:
public class FingerPrint
{
public DateTime TimeStamp { get; } = DateTime.UtcNow;
public string User { get; } =
System.Security.Principal.WindowsPrincipal.Current.Identity.Name;
public string Process { get; } =
System.Diagnostics.Process.GetCurrentProcess().ProcessName;
}
Предупреждающие примечания
Постарайтесь не путать инициализаторы авто-свойств или поля с похожими способами выражения-тела, которые используют =>
в противоположность =
, и поля, которые не включают { get; }
.
Например, каждая из следующих деклараций различна.
public class UserGroupDto
{
// Read-only auto-property with initializer:
public ICollection<UserDto> Users1 { get; } = new HashSet<UserDto>();
// Read-write field with initializer:
public ICollection<UserDto> Users2 = new HashSet<UserDto>();
// Read-only auto-property with expression body:
public ICollection<UserDto> Users3 => new HashSet<UserDto>();
}
Отсутствует { get; }
в объявлении свойства приводит к публичному полю. И автообновление Users1
чтения, и поле чтения-записи Users2
инициализируются только один раз, но общедоступное поле позволяет изменять экземпляр коллекции вне класса, что обычно нежелательно. Изменение автоматического свойства только для чтения с телом выражения в свойство только для чтения с инициализатором требует не только удаления >
from =>
, но и добавления { get; }
.
Users3
символ ( =>
вместо =
) в Users3
приводит к каждому доступу к свойству, возвращающему новый экземпляр HashSet<UserDto>
который, хотя действительный C # (с точки зрения компилятора) вряд ли будет желательным, когда используется для члена коллекции.
Вышеприведенный код эквивалентен:
public class UserGroupDto
{
// This is a property returning the same instance
// which was created when the UserGroupDto was instantiated.
private ICollection<UserDto> _users1 = new HashSet<UserDto>();
public ICollection<UserDto> Users1 { get { return _users1; } }
// This is a field returning the same instance
// which was created when the UserGroupDto was instantiated.
public virtual ICollection<UserDto> Users2 = new HashSet<UserDto>();
// This is a property which returns a new HashSet<UserDto> as
// an ICollection<UserDto> on each call to it.
public ICollection<UserDto> Users3 { get { return new HashSet<UserDto>(); } }
}
Инициализаторы индекса
Инициализаторы индексов позволяют одновременно создавать и инициализировать объекты с индексами.
Это упрощает инициализацию словарей:
var dict = new Dictionary<string, int>()
{
["foo"] = 34,
["bar"] = 42
};
Любой объект, который имеет индексированный getter или setter, может использоваться с этим синтаксисом:
class Program
{
public class MyClassWithIndexer
{
public int this[string index]
{
set
{
Console.WriteLine($"Index: {index}, value: {value}");
}
}
}
public static void Main()
{
var x = new MyClassWithIndexer()
{
["foo"] = 34,
["bar"] = 42
};
Console.ReadKey();
}
}
Выход:
Индекс: foo, значение: 34
Индекс: bar, значение: 42
Если класс имеет несколько индексаторов, их можно назначить в одной группе операторов:
class Program
{
public class MyClassWithIndexer
{
public int this[string index]
{
set
{
Console.WriteLine($"Index: {index}, value: {value}");
}
}
public string this[int index]
{
set
{
Console.WriteLine($"Index: {index}, value: {value}");
}
}
}
public static void Main()
{
var x = new MyClassWithIndexer()
{
["foo"] = 34,
["bar"] = 42,
[10] = "Ten",
[42] = "Meaning of life"
};
}
}
Выход:
Индекс: foo, значение: 34
Индекс: bar, значение: 42
Индекс: 10, значение: Десять
Индекс: 42, значение: Значение жизни
Следует отметить, что аксессор set
индексов может вести себя по-разному по сравнению с методом Add
(используется в инициализаторах коллекции).
Например:
var d = new Dictionary<string, int>
{
["foo"] = 34,
["foo"] = 42,
}; // does not throw, second value overwrites the first one
против:
var d = new Dictionary<string, int>
{
{ "foo", 34 },
{ "foo", 42 },
}; // run-time ArgumentException: An item with the same key has already been added.
Строчная интерполяция
Строковая интерполяция позволяет разработчику комбинировать variables
и текст для формирования строки.
Основной пример
int
две переменные int
: foo
и bar
.
int foo = 34;
int bar = 42;
string resultString = $"The foo is {foo}, and the bar is {bar}.";
Console.WriteLine(resultString);
Выход :
Foo - 34, а бара - 42.
Скобки внутри строк все еще можно использовать, например:
var foo = 34;
var bar = 42;
// String interpolation notation (new style)
Console.WriteLine($"The foo is {{foo}}, and the bar is {{bar}}.");
Это дает следующий результат:
Foo - {foo}, а bar - {bar}.
Использование интерполяции со стенографическими строками verbatim
Использование @
перед строкой приведет к тому, что строка будет интерпретирована дословно. Таким образом, например, символы Unicode или разрывы строк будут оставаться такими же, как они были напечатаны. Однако это не повлияет на выражения в интерполированной строке, как показано в следующем примере:
Console.WriteLine($@"In case it wasn't clear:
\u00B9
The foo
is {foo},
and the bar
is {bar}.");
Выход: В случае непонятности:
\ u00B9
Foo
составляет 34,
и бар
составляет 42.
Выражения
При строковой интерполяции выражения в фигурных скобках {}
также могут быть оценены. Результат будет вставлен в соответствующее место в строке. Например, чтобы вычислить максимум foo
и bar
и вставить его, используйте Math.Max
в фигурных скобках:
Console.WriteLine($"And the greater one is: { Math.Max(foo, bar) }");
Выход:
И тем больше: 42
Примечание. Любые ведущие или конечные пробелы (включая пробел, вкладку и CRLF / новую строку) между фигурной скобкой и выражением полностью игнорируются и не включаются в вывод
В качестве другого примера переменные могут быть отформатированы как валюта:
Console.WriteLine($"Foo formatted as a currency to 4 decimal places: {foo:c4}");
Выход:
Foo отформатирован как валюта до 4 знаков после запятой: $ 34,0000
Или они могут быть отформатированы как даты:
Console.WriteLine($"Today is: {DateTime.Today:dddd, MMMM dd - yyyy}");
Выход:
Сегодня: понедельник, 20 июля - 2015 г.
Операции с условным (тройным) оператором также могут быть оценены внутри интерполяции. Однако они должны быть заключены в круглые скобки, поскольку двоеточие в противном случае используется для указания форматирования, как показано выше:
Console.WriteLine($"{(foo > bar ? "Foo is larger than bar!" : "Bar is larger than foo!")}");
Выход:
Бар больше, чем foo!
Условные выражения и спецификаторы формата могут быть смешаны:
Console.WriteLine($"Environment: {(Environment.Is64BitProcess ? 64 : 32):00'-bit'} process");
Выход:
Окружающая среда: 32-битный процесс
Эквивалентные последовательности
Символы обратной косой черты ( \
) и quote ( "
) работают точно так же в интерполированных строках, как и в неинтерполированных строках, как для дословных, так и для невербальных строковых литералов:
Console.WriteLine($"Foo is: {foo}. In a non-verbatim string, we need to escape \" and \\ with backslashes.");
Console.WriteLine($@"Foo is: {foo}. In a verbatim string, we need to escape "" with an extra quote, but we don't need to escape \");
Выход:
Foo - 34. В строке, отличной от слов, нам нужно избежать «и \ с обратными косыми чертами.
Foo - 34. В стенографической строке нам нужно сбежать с дополнительной цитатой, но нам не нужно бежать \
Чтобы включить фигурные скобки {
или }
в интерполированной строке, используйте две фигурные скобки {{
или }}
:
$"{{foo}} is: {foo}"
Выход:
{foo}: 34
Тип FormattableString
Тип выражения интерполяции строки $"..."
не всегда является простой строкой. Компилятор решает, какой тип назначить в зависимости от контекста:
string s = $"hello, {name}";
System.FormattableString s = $"Hello, {name}";
System.IFormattable s = $"Hello, {name}";
Это также порядок предпочтений типа, когда компилятору необходимо выбрать, какой перегруженный метод будет вызываться.
Новый тип , System.FormattableString
, представляет собой строку составного формата вместе с аргументами, которые нужно отформатировать. Используйте это, чтобы писать приложения, которые обрабатывают аргументы интерполяции:
public void AddLogItem(FormattableString formattableString)
{
foreach (var arg in formattableString.GetArguments())
{
// do something to interpolation argument 'arg'
}
// use the standard interpolation and the current culture info
// to get an ordinary String:
var formatted = formattableString.ToString();
// ...
}
Вызовите вышеуказанный метод с помощью:
AddLogItem($"The foo is {foo}, and the bar is {bar}.");
Например, можно было бы отказаться от затрат на производительность форматирования строки, если уровень ведения журнала уже собирался отфильтровать элемент журнала. Неявные преобразования
Имеются неявные преобразования типов из интерполированной строки:
var s = $"Foo: {foo}";
System.IFormattable s = $"Foo: {foo}";
Вы также можете создать переменную IFormattable
которая позволяет вам преобразовать строку с инвариантным контекстом: var s = $"Bar: {bar}";
System.FormattableString s = $"Bar: {bar}";
Методы текущей и инвариантной культуры
Если анализ кода включен, интерполированные строки будут выдавать предупреждение CA1305 (Укажите IFormatProvider
). Статический метод может использоваться для применения текущей культуры.
public static class Culture
{
public static string Current(FormattableString formattableString)
{
return formattableString?.ToString(CultureInfo.CurrentCulture);
}
public static string Invariant(FormattableString formattableString)
{
return formattableString?.ToString(CultureInfo.InvariantCulture);
}
}
Затем, чтобы создать правильную строку для текущей культуры, просто используйте выражение:
Culture.Current($"interpolated {typeof(string).Name} string.")
Culture.Invariant($"interpolated {typeof(string).Name} string.")
Примечание : Current
и Invariant
не могут быть созданы как методы расширения, потому что по умолчанию компилятор присваивает тип String
интерполированному строковому выражению, из-за которого невозможно скомпилировать следующий код: $"interpolated {typeof(string).Name} string.".Current();
Класс FormattableString
уже содержит метод Invariant()
, поэтому самым простым способом перехода на инвариантную культуру является using static
:
using static System.FormattableString;
string invariant = Invariant($"Now = {DateTime.Now}"); string current = $"Now = {DateTime.Now}";
За кулисами
Интерполированные строки - это просто синтаксический сахар для String.Format()
. Компилятор ( Roslyn ) превратит его в String.Format
за кулисами:
var text = $"Hello {name + lastName}";
Вышеуказанное будет преобразовано в следующее:
string text = string.Format("Hello {0}", new object[] {
name + lastName
});
Интерполяция строк и Linq
Можно использовать интерполированные строки в операторах Linq для дальнейшего повышения удобочитаемости.
var fooBar = (from DataRow x in fooBarTable.Rows
select string.Format("{0}{1}", x["foo"], x["bar"])).ToList();
Может быть переписано как:
var fooBar = (from DataRow x in fooBarTable.Rows
select $"{x["foo"]}{x["bar"]}").ToList();
Многоразовые интерполированные строки
С помощью string.Format
вы можете создавать строки многократного использования:
public const string ErrorFormat = "Exception caught:\r\n{0}";
// ...
Logger.Log(string.Format(ErrorFormat, ex));
Интерполированные строки, однако, не будут компилироваться с заполнителями, ссылаясь на несуществующие переменные. Следующие не будут компилироваться:
public const string ErrorFormat = $"Exception caught:\r\n{error}";
// CS0103: The name 'error' does not exist in the current context
Вместо этого создайте Func<>
который использует переменные и возвращает String
:
public static Func<Exception, string> FormatError =
error => $"Exception caught:\r\n{error}";
// ...
Logger.Log(FormatError(ex));
Строчная интерполяция и локализация
Если вы локализируете свое приложение, вы можете задаться вопросом, можно ли использовать интерполяцию строк вместе с локализацией. Действительно, было бы неплохо иметь возможность хранить в файлах ресурсов String
s вроде:
"My name is {name} {middlename} {surname}"
вместо гораздо менее читаемого: "My name is {0} {1} {2}"
Процесс интерполяции String
происходит во время компиляции , в отличие от строки форматирования со строкой. string.Format
которая встречается во время выполнения . Выражения в интерполированной строке должны ссылаться на имена в текущем контексте и должны храниться в файлах ресурсов. Это означает, что если вы хотите использовать локализацию, вам нужно сделать это так:
var FirstName = "John";
// method using different resource file "strings"
// for French ("strings.fr.resx"), German ("strings.de.resx"),
// and English ("strings.en.resx")
void ShowMyNameLocalized(string name, string middlename = "", string surname = "")
{
// get localized string
var localizedMyNameIs = Properties.strings.Hello;
// insert spaces where necessary
name = (string.IsNullOrWhiteSpace(name) ? "" : name + " ");
middlename = (string.IsNullOrWhiteSpace(middlename) ? "" : middlename + " ");
surname = (string.IsNullOrWhiteSpace(surname) ? "" : surname + " ");
// display it
Console.WriteLine($"{localizedMyNameIs} {name}{middlename}{surname}".Trim());
}
// switch to French and greet John
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("fr-FR");
ShowMyNameLocalized(FirstName);
// switch to German and greet John
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("de-DE");
ShowMyNameLocalized(FirstName);
// switch to US English and greet John
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("en-US");
ShowMyNameLocalized(FirstName);
Если строки ресурсов для языков, используемых выше, правильно сохраняются в отдельных файлах ресурсов, вы должны получить следующий результат:
Bonjour, mon nom est John
Hallo, mein Имя ist John
Привет меня зовут Джон
Обратите внимание, что это означает, что имя следует за локализованной строкой на каждом языке. Если это не так, вам нужно добавить заполнители в строки ресурсов и изменить вышеприведенную функцию или вам нужно запросить информацию о культуре в этой функции и предоставить оператор case switch, содержащий разные случаи. Дополнительные сведения о файлах ресурсов см. В разделе Как использовать локализацию в C # .
Хорошей практикой является использование стандартного резервного языка, который большинство людей поймет, если перевод недоступен. Я предлагаю использовать английский язык по умолчанию.
Рекурсивная интерполяция
Хотя это и не очень полезно, разрешено использовать интерполированную string
рекурсивно внутри фигурных скобок другого:
Console.WriteLine($"String has {$"My class is called {nameof(MyClass)}.".Length} chars:");
Console.WriteLine($"My class is called {nameof(MyClass)}.");
Выход:
Строка имеет 27 символов:
Мой класс называется MyClass.
Ожидание в улове и, наконец,
Можно использовать выражение await
для применения оператора ожидания к задачам или задаче (из TResult) в catch
и, finally
блоках на C # 6.
Невозможно использовать выражение await
в catch
и, finally
блоки в более ранних версиях из-за ограничений компилятора. C # 6 делает ожидание асинхронных задач намного проще, разрешая выражение await
.
try
{
//since C#5
await service.InitializeAsync();
}
catch (Exception e)
{
//since C#6
await logger.LogAsync(e);
}
finally
{
//since C#6
await service.CloseAsync();
}
В C # 5 требовалось использовать bool
или объявить Exception
вне try catch для выполнения асинхронных операций. Этот метод показан в следующем примере:
bool error = false;
Exception ex = null;
try
{
// Since C#5
await service.InitializeAsync();
}
catch (Exception e)
{
// Declare bool or place exception inside variable
error = true;
ex = e;
}
// If you don't use the exception
if (error)
{
// Handle async task
}
// If want to use information from the exception
if (ex != null)
{
await logger.LogAsync(e);
}
// Close the service, since this isn't possible in the finally
await service.CloseAsync();
Нулевое распространение
?.
оператор и оператор ?[...]
называются нуль-условным оператором . Его иногда называют другие имена, такие как безопасный оператор навигации .
Это полезно, потому что если .
(член accessor) применяется к выражению, которое вычисляет значение null
, программа будет генерировать NullReferenceException
. Если разработчик вместо этого использует ?.
(нуль-условный) оператор, выражение будет вычисляться как null вместо того, чтобы бросать исключение.
Заметим, что если ?.
используется оператор, а выражение не равно null ?.
и .
эквивалентны.
основы
var teacherName = classroom.GetTeacher().Name;
// throws NullReferenceException if GetTeacher() returns null
Если classroom
не имеет учителя, GetTeacher()
может возвращать значение null
. Когда он равен null
и к нему будет применено свойство Name
, будет NullReferenceException
.
Если мы изменим это утверждение, чтобы использовать ?.
синтаксис, результат всего выражения будет null
:
var teacherName = classroom.GetTeacher()?.Name;
// teacherName is null if GetTeacher() returns null
Впоследствии, если classroom
также может быть null
, мы могли бы также написать это утверждение как:
var teacherName = classroom?.GetTeacher()?.Name;
// teacherName is null if GetTeacher() returns null OR classroom is null
Это пример короткого замыкания: когда любая операция условного доступа, использующая оператор с нулевым условием, имеет значение null, все выражение немедленно вычисляется до нуля, без обработки остальной части цепочки.
Когда терминальный член выражения, содержащего оператор с нулевым условием, имеет тип значения, выражение оценивается как Nullable<T>
этого типа и поэтому не может использоваться как прямая замена выражения без ?.
,
bool hasCertification = classroom.GetTeacher().HasCertification;
// compiles without error but may throw a NullReferenceException at runtime
bool hasCertification = classroom?.GetTeacher()?.HasCertification;
// compile time error: implicit conversion from bool? to bool not allowed
bool? hasCertification = classroom?.GetTeacher()?.HasCertification;
// works just fine, hasCertification will be null if any part of the chain is null
bool hasCertification = classroom?.GetTeacher()?.HasCertification.GetValueOrDefault();
// must extract value from nullable to assign to a value type variable
Использовать с Null-Coalescing Operator (??)
Вы можете комбинировать оператор с нулевым условием с Null-coalescing Operator ( ??
), чтобы вернуть значение по умолчанию, если выражение разрешает null
. Используя наш пример выше:
var teacherName = classroom?.GetTeacher()?.Name ?? "No Name";
// teacherName will be "No Name" when GetTeacher()
// returns null OR classroom is null OR Name is null
Использование с индексаторами
Оператор с нулевым условием может использоваться с индексаторами :
var firstStudentName = classroom?.Students?[0]?.Name;
В приведенном выше примере:
- Первый
?.
гарантирует, чтоclassroom
не являетсяnull
. - Второй
?
гарантирует, что вся коллекцияStudents
не равнаnull
. - Третий
?.
после того, как индексатор гарантирует, что индексирующее устройство[0]
не вернетnull
объект. Следует отметить, что эта операция все равно может вызватьIndexOutOfRangeException
.
Использование с функциями void
Нуль-условный оператор также может использоваться с функциями void
. Однако в этом случае оператор не будет оценивать значение null
. Это просто предотвратит исключение NullReferenceException
.
List<string> list = null;
list?.Add("hi"); // Does not evaluate to null
Использование с помощью вызова события
Предполагая следующее определение события:
private event EventArgs OnCompleted;
При вызове события традиционно лучше всего проверять, является ли событие null
если нет подписчиков:
var handler = OnCompleted;
if (handler != null)
{
handler(EventArgs.Empty);
}
Поскольку введен оператор с нулевым условием, вызов можно свести к одной строке:
OnCompleted?.Invoke(EventArgs.Empty);
Ограничения
Null-условный оператор производит rvalue, а не lvalue, то есть он не может использоваться для присвоения свойств, подписки на события и т. Д. Например, следующий код не будет работать:
// Error: The left-hand side of an assignment must be a variable, property or indexer
Process.GetProcessById(1337)?.EnableRaisingEvents = true;
// Error: The event can only appear on the left hand side of += or -=
Process.GetProcessById(1337)?.Exited += OnProcessExited;
Gotchas
Обратите внимание, что:
int? nameLength = person?.Name.Length; // safe if 'person' is null
это не то же самое, что:
int? nameLength = (person?.Name).Length; // avoid this
потому что первое соответствует:
int? nameLength = person != null ? (int?)person.Name.Length : null;
и последнее соответствует:
int? nameLength = (person != null ? person.Name : null).Length;
Несмотря на тернарный оператор ?:
Используется здесь для объяснения разницы между двумя случаями, эти операторы не эквивалентны. Это можно легко продемонстрировать в следующем примере:
void Main()
{
var foo = new Foo();
Console.WriteLine("Null propagation");
Console.WriteLine(foo.Bar?.Length);
Console.WriteLine("Ternary");
Console.WriteLine(foo.Bar != null ? foo.Bar.Length : (int?)null);
}
class Foo
{
public string Bar
{
get
{
Console.WriteLine("I was read");
return string.Empty;
}
}
}
Какие результаты:
Нулевое распространение
Я был прочитан
0
троичный
Я был прочитан
Я был прочитан
0
Чтобы избежать эквивалента нескольких вызовов, можно:
var interimResult = foo.Bar;
Console.WriteLine(interimResult != null ? interimResult.Length : (int?)null);
И эта разница несколько объясняет, почему оператор распространения пустоты еще не поддерживается в деревьях выражений.
Использование статического типа
using static [Namespace.Type]
директивы позволяет импортировать статические элементы типов и значений перечисления. Методы расширения импортируются как методы расширения (всего один тип), а не в область верхнего уровня.
using static System.Console;
using static System.ConsoleColor;
using static System.Math;
class Program
{
static void Main()
{
BackgroundColor = DarkBlue;
WriteLine(Sqrt(2));
}
}
using System;
class Program
{
static void Main()
{
Console.BackgroundColor = ConsoleColor.DarkBlue;
Console.WriteLine(Math.Sqrt(2));
}
}
Улучшенное разрешение перегрузки
Следующий фрагмент показывает пример передачи группы методов (в отличие от лямбда), когда ожидается делегат. Разрешение перегрузки теперь разрешит это вместо повышения неоднозначной ошибки перегрузки из-за возможности C # 6 проверить тип возвращаемого метода.
using System;
public class Program
{
public static void Main()
{
Overloaded(DoSomething);
}
static void Overloaded(Action action)
{
Console.WriteLine("overload with action called");
}
static void Overloaded(Func<int> function)
{
Console.WriteLine("overload with Func<int> called");
}
static int DoSomething()
{
Console.WriteLine(0);
return 0;
}
}
Результаты:
ошибка
ошибка CS0121: вызов неоднозначен между следующими методами или свойствами: «Program.Overloaded (System.ction)» и «Program.Overloaded (System.Func)»
C # 6 также может хорошо обработать следующий случай точного соответствия для лямбда-выражений, что привело бы к ошибке в C # 5 .
using System;
class Program
{
static void Foo(Func<Func<long>> func) {}
static void Foo(Func<Func<int>> func) {}
static void Main()
{
Foo(() => () => 7);
}
}
Незначительные изменения и исправления
Круглые скобки теперь запрещены по именованным параметрам. Следующие компиляции в C # 5, но не C # 6
Console.WriteLine((value: 23));
Операнды is
и as
больше не могут быть группами методов. Следующие компиляции в C # 5, но не C # 6
var result = "".Any is byte;
Собственный компилятор допустил это (хотя он и сделал предупреждение), и на самом деле даже не проверял совместимость метода расширения, позволяя сумасшедшие вещи, такие как
1.Any is string
илиIDisposable.Dispose is object
.
См. Эту ссылку для получения обновлений об изменениях.
Использование метода расширения для инициализации коллекции
Синтаксис инициализации синтаксиса можно использовать при создании экземпляра любого класса, который реализует IEnumerable
и имеет метод Add
который принимает один параметр.
В предыдущих версиях этот метод Add
должен был быть методом экземпляра для инициализированного класса. В C # 6 он также может быть методом расширения.
public class CollectionWithAdd : IEnumerable
{
public void Add<T>(T item)
{
Console.WriteLine("Item added with instance add method: " + item);
}
public IEnumerator GetEnumerator()
{
// Some implementation here
}
}
public class CollectionWithoutAdd : IEnumerable
{
public IEnumerator GetEnumerator()
{
// Some implementation here
}
}
public static class Extensions
{
public static void Add<T>(this CollectionWithoutAdd collection, T item)
{
Console.WriteLine("Item added with extension add method: " + item);
}
}
public class Program
{
public static void Main()
{
var collection1 = new CollectionWithAdd{1,2,3}; // Valid in all C# versions
var collection2 = new CollectionWithoutAdd{4,5,6}; // Valid only since C# 6
}
}
Это приведет к выводу:
Элемент добавлен с помощью метода добавления экземпляра: 1
Элемент добавлен с помощью метода добавления экземпляра: 2
Элемент добавлен с помощью метода добавления экземпляра: 3
Добавлен элемент с добавлением метода добавления: 4
Добавлен элемент с добавлением метода добавления: 5
Добавлен элемент с добавлением метода добавления: 6
Отключить улучшения предупреждений
В C # 5.0 и ранее разработчик мог только подавлять предупреждения по номеру. С введением Roslyn Analyzers C # нужен способ отключения предупреждений, выпущенных из конкретных библиотек. С C # 6.0 директива pragma может подавлять предупреждения по имени.
До:
#pragma warning disable 0501
C # 6.0:
#pragma warning disable CS0501