C# Language
Funkcje C # 6.0
Szukaj…
Wprowadzenie
Ta szósta iteracja języka C # jest dostarczana przez kompilator Roslyn. Ten kompilator wyszedł z wersją 4.6 .NET Framework, jednak może generować kod w sposób zgodny z poprzednimi wersjami, aby umożliwić celowanie we wcześniejsze wersje środowiska. Kod w wersji C # 6 można skompilować w pełni kompatybilny wstecz z .NET 4.0. Może być również używany do wcześniejszych frameworków, jednak niektóre funkcje wymagające dodatkowej obsługi frameworków mogą nie działać poprawnie.
Uwagi
Szósta wersja C # została wydana w lipcu 2015 r. Wraz z Visual Studio 2015 i .NET 4.6.
Oprócz dodania kilku nowych funkcji językowych zawiera on kompletne przepisanie kompilatora. Wcześniej csc.exe
był natywną aplikacją Win32 napisaną w C ++, z C # 6 jest teraz aplikacją zarządzaną .NET napisaną w C #. Przepisanie to było znane jako projekt „Roslyn”, a kod jest teraz open source i dostępny na GitHub .
Nazwa operatora
Operator nameof
zwraca nazwę elementu kodu jako string
. Jest to przydatne podczas zgłaszania wyjątków związanych z argumentami metod, a także podczas implementowania INotifyPropertyChanged
.
public string SayHello(string greeted)
{
if (greeted == null)
throw new ArgumentNullException(nameof(greeted));
Console.WriteLine("Hello, " + greeted);
}
Operator nameof
jest oceniany w czasie kompilacji i zmienia wyrażenie na literał łańcuchowy. Jest to również przydatne w przypadku ciągów nazwanych na podstawie elementu, który je udostępnia. Rozważ następujące:
public static class Strings
{
public const string Foo = nameof(Foo); // Rather than Foo = "Foo"
public const string Bar = nameof(Bar); // Rather than Bar = "Bar"
}
Ponieważ wyrażenia nameof
są nameof
kompilacji, można ich używać w atrybutach, etykietach case
, instrukcjach switch
itd.
Wygodne jest używanie nameof
z Enum
s. Zamiast:
Console.WriteLine(Enum.One.ToString());
możliwe jest użycie:
Console.WriteLine(nameof(Enum.One))
W obu przypadkach wynikiem będzie One
.
Operator nameof
może uzyskiwać dostęp do elementów niestatycznych przy użyciu składni statycznej. Zamiast robić:
string foo = "Foo";
string lengthName = nameof(foo.Length);
Można zastąpić:
string lengthName = nameof(string.Length);
Dane wyjściowe będą mieć Length
w obu przykładach. Ten ostatni zapobiega jednak tworzeniu niepotrzebnych instancji.
Chociaż operator nameof
działa z większością konstrukcji językowych, istnieją pewne ograniczenia. Na przykład nie można używać operatora nameof
w przypadku otwartych typów ogólnych lub zwracanych wartości metod:
public static int Main()
{
Console.WriteLine(nameof(List<>)); // Compile-time error
Console.WriteLine(nameof(Main())); // Compile-time error
}
Ponadto, jeśli zastosujesz go do typu ogólnego, parametr typu ogólnego zostanie zignorowany:
Console.WriteLine(nameof(List<int>)); // "List"
Console.WriteLine(nameof(List<bool>)); // "List"
Więcej przykładów znajduje się w tym temacie poświęconym nameof
.
Obejście poprzednich wersji ( więcej szczegółów )
Chociaż operator nameof
nie istnieje w języku C # w wersjach wcześniejszych niż 6.0, podobną funkcjonalność można uzyskać za pomocą funkcji MemberExpression
jak poniżej:
Wyrażenie:
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;
}
Stosowanie:
string variableName = NameOf(() => variable);
string propertyName = NameOf((Foo o) => o.Bar);
Zauważ, że takie podejście powoduje utworzenie drzewa wyrażeń przy każdym wywołaniu, więc wydajność jest znacznie gorsza w porównaniu do operatora nameof
, który jest oceniany w czasie kompilacji i ma zerowe obciążenie w czasie wykonywania.
Elementy funkcyjne wyrażone w wyrażeniach
Elementy funkcyjne wyrażone w wyrażeniach pozwalają na użycie wyrażeń lambda jako obiektów składowych. W przypadku prostych członków może to skutkować czystszym i bardziej czytelnym kodem.
Funkcje oparte na wyrażeniach mogą być używane dla właściwości, indeksatorów, metod i operatorów.
Nieruchomości
public decimal TotalPrice => BasePrice + Taxes;
Jest równa:
public decimal TotalPrice
{
get
{
return BasePrice + Taxes;
}
}
Gdy z właściwością używana jest funkcja wyrażenia, właściwość jest implementowana jako właściwość tylko do pobierania.
Indeksujący
public object this[string key] => dictionary[key];
Jest równa:
public object this[string key]
{
get
{
return dictionary[key];
}
}
Metody
static int Multiply(int a, int b) => a * b;
Jest równa:
static int Multiply(int a, int b)
{
return a * b;
}
Które mogą być również używane z metodami void
:
public void Dispose() => resource?.Dispose();
ToString
można dodać do klasy Pair<T>
:
public override string ToString() => $"{First}, {Second}";
Ponadto to uproszczone podejście działa ze słowem kluczowym override
:
public class Foo
{
public int Bar { get; }
public string override ToString() => $"Bar: {Bar}";
}
Operatorzy
Mogą to również wykorzystać operatorzy:
public class Land
{
public double Area { get; set; }
public static Land operator +(Land first, Land second) =>
new Land { Area = first.Area + second.Area };
}
Ograniczenia
Elementy funkcyjne wyrażone w wyrażeniach mają pewne ograniczenia. Nie mogą zawierać instrukcji blokowych ani żadnych innych instrukcji zawierających bloki: if
, switch
, for
, foreach
, while
, do
, try
itp.
Niektóre instrukcje if
można zastąpić operatorami trójskładnikowymi. Niektóre instrukcje for
i foreach
można konwertować na zapytania LINQ, na przykład:
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());
We wszystkich innych przypadkach można zastosować starą składnię elementów członkowskich.
Elementy funkcyjne wyrażone w wyrażeniach mogą zawierać async
/ await
, ale często są nadmiarowe:
async Task<int> Foo() => await Bar();
Można zastąpić:
Task<int> Foo() => Bar();
Filtry wyjątków
Filtry wyjątków dają programistom możliwość dodania warunku (w postaci wyrażenia boolean
) do bloku catch , pozwalając na wykonanie catch
tylko wtedy, gdy warunek zostanie true
.
Filtry wyjątków umożliwiają propagację informacji debugowania w oryginalnym wyjątku, przy czym użycie instrukcji if
wewnątrz bloku catch
i ponowne zgłoszenie wyjątku zatrzymuje propagację informacji debugowania w oryginalnym wyjątku. W przypadku filtrów wyjątków wyjątek kontynuuje propagację w górę w stosie wywołań, chyba że warunek jest spełniony. W rezultacie filtry wyjątków znacznie ułatwiają debugowanie. Zamiast zatrzymywać się na instrukcji throw
, debugger zatrzyma się na instrukcji zgłaszającej wyjątek, z zachowanym bieżącym stanem i wszystkimi zmiennymi lokalnymi. Zrzuty zderzeniowe działają w podobny sposób.
Filtry wyjątków są obsługiwane przez CLR od samego początku i są dostępne z VB.NET i F # od ponad dekady, ujawniając część modelu obsługi wyjątków CLR. Dopiero po wydaniu wersji C # 6.0 funkcjonalność była dostępna również dla programistów C #.
Korzystanie z filtrów wyjątków
Filtry wyjątków są wykorzystywane przez dodanie klauzuli when
do wyrażenia catch
. Możliwe jest użycie dowolnego wyrażenia zwracającego wartość bool
w klauzuli when
(z wyjątkiem oczekiwania ). Deklarowana zmienna wyjątku ex
jest dostępna z poziomu klauzuli when
:
var SqlErrorToIgnore = 123;
try
{
DoSQLOperations();
}
catch (SqlException ex) when (ex.Number != SqlErrorToIgnore)
{
throw new Exception("An error occurred accessing the database", ex);
}
Wiele bloków catch
z klauzulami when
można łączyć. Pierwszy, when
zwracana jest klauzula true
, spowoduje przechwycenie wyjątku. Jego blok catch
zostanie wprowadzony, a pozostałe klauzule catch
zostaną zignorowane (ich klauzule when
nie będą oceniane). Na przykład:
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
{ ... }
Ryzykowne, gdy klauzula
Uwaga
Korzystanie z filtrów wyjątków może być ryzykowne: gdy
Exception
zostanie zgłoszony w ramach klauzuliwhen
,Exception
od klauzuliwhen
jest ignorowany i traktowany jakofalse
. Takie podejście umożliwia programistom pisanie klauzuliwhen
bez zajmowania się nieważnymi przypadkami.
Poniższy przykład ilustruje taki scenariusz:
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);
}
Zauważ, że filtry wyjątków pozwalają uniknąć mylących problemów z numerami linii związanych z używaniem throw
gdy kod błędu jest w tej samej funkcji. Na przykład w tym przypadku numer linii jest zgłaszany jako 6 zamiast 3:
1. int a = 0, b = 0;
2. try {
3. int c = a / b;
4. }
5. catch (DivideByZeroException) {
6. throw;
7. }
Numer wiersza wyjątku jest zgłaszany jako 6, ponieważ błąd został wychwycony i ponownie zgłoszony za pomocą instrukcji throw
w wierszu 6.
To samo nie dzieje się z filtrami wyjątków:
1. int a = 0, b = 0;
2. try {
3. int c = a / b;
4. }
5. catch (DivideByZeroException) when (a != 0) {
6. throw;
7. }
W tym przykładzie a
wynosi 0, a następnie klauzula catch
jest ignorowana, ale 3 jest zgłaszane jako numer wiersza. Jest tak, ponieważ nie rozwijają stosu . Dokładniej, wyjątek nie zostanie złapany na linii 5, ponieważ w rzeczywistości nie równe a
0
, a tym samym nie ma możliwości za wyjątkiem być ponownie rzucony na linii 6, ponieważ linia 6 nie wykonuje.
Logowanie jako efekt uboczny
Wywołania metod w tym stanie mogą powodować skutki uboczne, więc filtry wyjątków mogą być używane do uruchamiania kodu na wyjątkach bez ich wychwytywania. Typowym przykładem, który to wykorzystuje, jest metoda Log
, która zawsze zwraca wartość false
. Umożliwia to śledzenie informacji z dziennika podczas debugowania bez potrzeby ponownego zgłaszania wyjątku.
Należy pamiętać, że chociaż wydaje się to wygodnym sposobem rejestrowania, może być ryzykowne, zwłaszcza jeśli używane są zestawy rejestrujące innych firm. Mogą one generować wyjątki podczas logowania w nieoczywistych sytuacjach, które mogą nie zostać łatwo wykryte (patrz ryzykowna,
when(...)
klauzula powyżej).
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;
}
Powszechnym podejściem w poprzednich wersjach C # było rejestrowanie i ponowne zgłaszanie wyjątku.
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);
}
W finally
blok
Blok na finally
wykonywany za każdym razem, czy wyjątek jest zgłaszany, czy nie. Jedna subtelność z wyrażeniami określającymi, when
filtry wyjątków są wykonywane na górze stosu, zanim wejdą do wewnętrznych bloków finally
. Może to powodować nieoczekiwane wyniki i zachowania, gdy kod próbuje zmodyfikować stan globalny (np. Użytkownik lub kultura bieżącego wątku) i ustawić go z powrotem w finally
bloku.
Przykład: w finally
zablokuj
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");
}
}
Wyprodukowana produkcja:
Początek
Ocenia: Prawda
W końcu wewnętrzny
Złapać
Nareszcie wreszcie
W powyższym przykładzie, jeśli metoda SomeOperation
nie chce „wyciekać” globalnego stanu zmienia się na wywołujące klauzule when
, powinna również zawierać blok catch
do modyfikowania stanu. Na przykład:
private static void SomeOperation()
{
try
{
Flag = true;
throw new Exception("Boom");
}
catch
{
Flag = false;
throw;
}
finally
{
Flag = false;
Console.WriteLine("Inner Finally");
}
}
Często zdarza się, że klasy pomocnicze IDisposable
wykorzystują semantykę używania bloków do osiągnięcia tego samego celu, co IDisposable.Dispose
zawsze będzie wywoływana, zanim wyjątek wywoływany w bloku using
zacznie bulgotać w stosie.
Inicjatory właściwości automatycznych
Wprowadzenie
Właściwości można zainicjować za pomocą operatora =
po zamknięciu }
. Poniższa klasa Coordinate
pokazuje dostępne opcje inicjowania właściwości:
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
}
Akcesoria o różnej widoczności
Możesz zainicjować właściwości automatyczne, które mają różną widoczność na swoich akcesoriach. Oto przykład z chronionym ustawiaczem:
public string Name { get; protected set; } = "Cheeze";
Akcesorium może być również internal
, internal protected
lub private
.
Właściwości tylko do odczytu
Oprócz elastyczności i widoczności można także inicjować właściwości automatyczne tylko do odczytu. Oto przykład:
public List<string> Ingredients { get; } =
new List<string> { "dough", "sauce", "cheese" };
Ten przykład pokazuje również, jak zainicjować właściwość o typie złożonym. Ponadto właściwości automatyczne nie mogą być tylko do zapisu, co wyklucza również inicjalizację tylko do zapisu.
Stary styl (wcześniej niż C # 6.0)
Przed C # 6 wymagało to znacznie więcej pełnego kodu. Użyliśmy jednej dodatkowej zmiennej o nazwie backing property dla tej właściwości, aby podać wartość domyślną lub zainicjować właściwość publiczną, jak poniżej,
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;
}
}
Uwaga: Przed wersją C # 6.0 nadal można było inicjować odczytywanie i zapisywanie automatycznie zaimplementowanych właściwości (właściwości z narzędziem pobierającym i ustawiającym) z poziomu konstruktora, ale nie można było zainicjować właściwości zgodnie z jej deklaracją
Stosowanie
Inicjatory muszą oceniać wyrażenia statyczne, podobnie jak inicjalizatory pól. Jeśli potrzebujesz odwoływać się do elementów niestatycznych, możesz albo zainicjować właściwości w konstruktorach, jak poprzednio, lub użyć właściwości zawierających wyrażenie. Wyrażenia niestatyczne, takie jak poniższe (skomentowane), wygenerują błąd kompilatora:
// public decimal X { get; set; } = InitMe(); // generates compiler error
decimal InitMe() { return 4m; }
Ale do zainicjowania właściwości automatycznych można użyć metod statycznych:
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;
}
}
Metodę tę można również zastosować do właściwości z różnym poziomem akcesoriów:
public short Type { get; private set; } = 15;
Inicjator auto-właściwości umożliwia przypisanie właściwości bezpośrednio w ramach ich deklaracji. W przypadku właściwości tylko do odczytu dba o wszystkie wymagania wymagane do zapewnienia niezmienności właściwości. Rozważmy na przykład klasę FingerPrint
w następującym przykładzie:
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;
}
Uwagi ostrzegawcze
Uważaj, aby nie pomylić inicjatorów właściwości automatycznych lub pól z podobnie wyglądającymi metodami wyrażenia-ciała, które używają =>
w przeciwieństwie do =
oraz pól, które nie zawierają { get; }
.
Na przykład każda z następujących deklaracji jest inna.
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>();
}
Missing { get; }
w deklaracji właściwości powoduje wyświetlenie pola publicznego. Zarówno auto-właściwość Users1
do odczytu Users1
i pole Do odczytu i zapisu Users2
są inicjowane tylko raz, ale pole publiczne umożliwia zmianę instancji kolekcji spoza klasy, co zwykle jest niepożądane. Zmiana właściwości automatycznej tylko do odczytu z treścią wyrażenia na właściwość tylko do odczytu za pomocą inicjatora wymaga nie tylko usunięcia >
z =>
, ale także dodania { get; }
.
Inny symbol ( =>
zamiast =
) w Users3
powoduje, że każdy dostęp do właściwości zwraca nową instancję HashSet<UserDto>
która, choć jest poprawna w C # (z punktu widzenia kompilatora), raczej nie będzie pożądanym zachowaniem, gdy używane dla członka kolekcji.
Powyższy kod jest równoważny z:
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>(); } }
}
Inicjatory indeksu
Inicjatory indeksów umożliwiają jednoczesne tworzenie i inicjowanie obiektów za pomocą indeksów.
Dzięki temu inicjowanie słowników jest bardzo łatwe:
var dict = new Dictionary<string, int>()
{
["foo"] = 34,
["bar"] = 42
};
Dowolny obiekt, który ma indeksujący getter lub setter może być używany z tą składnią:
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();
}
}
Wynik:
Indeks: foo, wartość: 34
Indeks: bar, wartość: 42
Jeśli klasa ma wiele indeksatorów, możliwe jest przypisanie ich wszystkich do jednej grupy instrukcji:
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"
};
}
}
Wynik:
Indeks: foo, wartość: 34
Indeks: bar, wartość: 42
Indeks: 10, wartość: Dziesięć
Indeks: 42, wartość: Sens życia
Należy zauważyć, że akcesorium set
indeksującego może zachowywać się inaczej niż metoda Add
(używana w inicjalizatorach kolekcji).
Na przykład:
var d = new Dictionary<string, int>
{
["foo"] = 34,
["foo"] = 42,
}; // does not throw, second value overwrites the first one
przeciw:
var d = new Dictionary<string, int>
{
{ "foo", 34 },
{ "foo", 42 },
}; // run-time ArgumentException: An item with the same key has already been added.
Interpolacja ciągów
Interpolacja ciągów umożliwia programistom łączenie variables
i tekstu w celu utworzenia ciągu.
Podstawowy przykład
Tworzone są dwie zmienne int
: foo
i bar
.
int foo = 34;
int bar = 42;
string resultString = $"The foo is {foo}, and the bar is {bar}.";
Console.WriteLine(resultString);
Wyjście :
Foo wynosi 34, a pasek ma 42.
Nadal można używać nawiasów klamrowych:
var foo = 34;
var bar = 42;
// String interpolation notation (new style)
Console.WriteLine($"The foo is {{foo}}, and the bar is {{bar}}.");
Daje to następujące dane wyjściowe:
Foo to {foo}, a pasek to {bar}.
Używanie interpolacji z dosłownymi literałami ciągów
Użycie @
przed ciągiem spowoduje, że będzie on interpretowany dosłownie. Na przykład znaki Unicode lub podziały wierszy pozostaną dokładnie tak, jak zostały wpisane. Nie wpłynie to jednak na wyrażenia w interpolowanym ciągu, jak pokazano w poniższym przykładzie:
Console.WriteLine($@"In case it wasn't clear:
\u00B9
The foo
is {foo},
and the bar
is {bar}.");
Wynik: W przypadku, gdy nie było jasne:
\ u00B9
Foo
ma 34 lata,
i pasek
ma 42 lata
Wyrażenia
Dzięki interpolacji ciągów można również oceniać wyrażenia w nawiasach klamrowych {}
. Wynik zostanie wstawiony w odpowiednim miejscu w ciągu. Na przykład, aby obliczyć maksimum foo
i bar
i wstawić je, użyj Math.Max
w nawiasach klamrowych:
Console.WriteLine($"And the greater one is: { Math.Max(foo, bar) }");
Wynik:
A większy to: 42
Uwaga: Wszelkie początkowe lub końcowe białe spacje (w tym spacja, tabulator i CRLF / nowa linia) między nawiasami klamrowymi a wyrażeniem są całkowicie ignorowane i nie są uwzględniane w danych wyjściowych
Jako kolejny przykład zmienne można sformatować jako walutę:
Console.WriteLine($"Foo formatted as a currency to 4 decimal places: {foo:c4}");
Wynik:
Foo sformatowane jako waluta z dokładnością do 4 miejsc po przecinku: 34,0000 $
Lub mogą być sformatowane jako daty:
Console.WriteLine($"Today is: {DateTime.Today:dddd, MMMM dd - yyyy}");
Wynik:
Dzisiaj jest: poniedziałek, 20 lipca 2015 r
Instrukcje z operatorem warunkowym (trójskładnikowym) można również oceniać w ramach interpolacji. Jednak muszą być one owinięte w nawiasy, ponieważ dwukropek jest inaczej używany do wskazania formatowania, jak pokazano powyżej:
Console.WriteLine($"{(foo > bar ? "Foo is larger than bar!" : "Bar is larger than foo!")}");
Wynik:
Bar jest większy niż foo!
Wyrażenia warunkowe i specyfikatory formatu można mieszać:
Console.WriteLine($"Environment: {(Environment.Is64BitProcess ? 64 : 32):00'-bit'} process");
Wynik:
Środowisko: proces 32-bitowy
Sekwencje ewakuacyjne
Zmiana znaczenia znaków odwrotnego ukośnika ( \
) i cudzysłowu ( "
) działa dokładnie tak samo w przypadku łańcuchów interpolowanych, jak i łańcuchów nieinterpolowanych, zarówno dla dosłownych, jak i nieliteralnych literałów:
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 \");
Wynik:
Foo to 34. W łańcuchu nieokreślonym musimy uciec „i \ z ukośnikami odwrotnymi.
Foo ma 34 lata. Ciągiem słownym musimy uciec „z dodatkowym cytatem, ale nie musimy uciekać \
Aby dołączyć nawias klamrowy {
lub }
do interpolowanego ciągu, użyj dwóch nawiasów klamrowych {{
lub }}
:
$"{{foo}} is: {foo}"
Wynik:
{foo} to: 34
Typ FormattableString
Typ wyrażenia interpolacji ciągu $"..."
nie zawsze jest prostym ciągiem. Kompilator decyduje, który typ przypisać w zależności od kontekstu:
string s = $"hello, {name}";
System.FormattableString s = $"Hello, {name}";
System.IFormattable s = $"Hello, {name}";
Jest to również kolejność preferencji typów, gdy kompilator musi wybrać, która przeciążona metoda ma zostać wywołana.
Nowy typ , System.FormattableString
, reprezentuje ciąg formatu złożonego wraz z argumentami do sformatowania. Użyj tego, aby napisać aplikacje, które konkretnie obsługują argumenty interpolacji:
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();
// ...
}
Wywołaj powyższą metodę za pomocą:
AddLogItem($"The foo is {foo}, and the bar is {bar}.");
Na przykład można nie ponosić kosztów wydajności związanych z formatowaniem łańcucha, jeśli poziom rejestrowania zamierzał już odfiltrować element dziennika. Niejawne konwersje
Istnieją niejawne konwersje typu z interpolowanego ciągu:
var s = $"Foo: {foo}";
System.IFormattable s = $"Foo: {foo}";
Możesz także utworzyć zmienną IFormattable
, która pozwala przekonwertować ciąg z niezmiennym kontekstem: var s = $"Bar: {bar}";
System.FormattableString s = $"Bar: {bar}";
Obecne i niezmienne metody hodowli
Jeśli analiza kodu jest włączona, interpolowane ciągi generują ostrzeżenie CA1305 (Określ IFormatProvider
). Do zastosowania bieżącej kultury można zastosować metodę statyczną.
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);
}
}
Następnie, aby utworzyć poprawny ciąg dla bieżącej kultury, wystarczy użyć wyrażenia:
Culture.Current($"interpolated {typeof(string).Name} string.")
Culture.Invariant($"interpolated {typeof(string).Name} string.")
Uwaga : Nie można utworzyć Current
i Invariant
jako metody rozszerzenia, ponieważ domyślnie kompilator przypisuje typ String
do interpolowanego wyrażenia ciąg, co powoduje, że następujący kod nie może się skompilować: $"interpolated {typeof(string).Name} string.".Current();
Klasa FormattableString
zawiera już metodę Invariant()
, więc najprostszym sposobem przejścia na kulturę niezmienną jest using static
:
using static System.FormattableString;
string invariant = Invariant($"Now = {DateTime.Now}"); string current = $"Now = {DateTime.Now}";
Za kulisami
Interpolowane ciągi znaków to po prostu cukier składniowy dla String.Format()
. Kompilator ( Roslyn ) zamieni go w String.Format
za kulisami:
var text = $"Hello {name + lastName}";
Powyższe zostaną przekonwertowane na coś takiego:
string text = string.Format("Hello {0}", new object[] {
name + lastName
});
Interpolacja ciągów znaków i Linq
Możliwe jest użycie interpolowanych ciągów w instrukcjach Linq w celu dalszego zwiększenia czytelności.
var fooBar = (from DataRow x in fooBarTable.Rows
select string.Format("{0}{1}", x["foo"], x["bar"])).ToList();
Może być przepisany jako:
var fooBar = (from DataRow x in fooBarTable.Rows
select $"{x["foo"]}{x["bar"]}").ToList();
Ciągi interpolowane wielokrotnego użytku
Za pomocą string.Format
możesz tworzyć ciągi formatu wielokrotnego użytku:
public const string ErrorFormat = "Exception caught:\r\n{0}";
// ...
Logger.Log(string.Format(ErrorFormat, ex));
Jednak interpolowane ciągi nie będą się kompilować z symbolami zastępczymi odnoszącymi się do nieistniejących zmiennych. Następujące elementy nie zostaną skompilowane:
public const string ErrorFormat = $"Exception caught:\r\n{error}";
// CS0103: The name 'error' does not exist in the current context
Zamiast tego utwórz Func<>
który zużywa zmienne i zwraca String
:
public static Func<Exception, string> FormatError =
error => $"Exception caught:\r\n{error}";
// ...
Logger.Log(FormatError(ex));
Interpolacja i lokalizacja łańcucha
Jeśli lokalizujesz swoją aplikację, możesz zastanawiać się, czy można użyć interpolacji ciągów wraz z lokalizacją. Rzeczywiście, byłoby miło mieć możliwość przechowywania w plikach zasobów String
takie jak:
"My name is {name} {middlename} {surname}"
zamiast znacznie mniej czytelnego: "My name is {0} {1} {2}"
Proces interpolacji String
zachodzi w czasie kompilacji , w przeciwieństwie do formatowania łańcucha za pomocą string.Format
który występuje w czasie wykonywania . Wyrażenia w interpolowanym ciągu muszą odwoływać się do nazw w bieżącym kontekście i muszą być przechowywane w plikach zasobów. Oznacza to, że jeśli chcesz użyć lokalizacji, musisz to zrobić w następujący sposób:
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);
Jeśli ciągi zasobów dla języków używanych powyżej są poprawnie przechowywane w poszczególnych plikach zasobów, powinieneś otrzymać następujące dane wyjściowe:
Bonjour, mon nom est John
Cześć, mein Name ist John
Cześć, mam na imię John
Zauważ, że oznacza to, że nazwa występuje po zlokalizowanym ciągu w każdym języku. Jeśli tak nie jest, musisz dodać symbole zastępcze do ciągów zasobów i zmodyfikować powyższą funkcję, lub zapytać o informacje o kulturze w funkcji i podać instrukcję zmiany instrukcji zawierającą różne przypadki. Aby uzyskać więcej informacji o plikach zasobów, zobacz Jak korzystać z lokalizacji w C # .
Dobrą praktyką jest używanie domyślnego języka zastępczego, który większość osób zrozumie, na wypadek, gdyby tłumaczenie nie było dostępne. Sugeruję używać angielskiego jako domyślnego języka zastępczego.
Interpolacja rekurencyjna
Chociaż nie jest to bardzo przydatne, można rekurencyjnie używać interpolowanego string
wewnątrz nawiasów klamrowych innej osoby:
Console.WriteLine($"String has {$"My class is called {nameof(MyClass)}.".Length} chars:");
Console.WriteLine($"My class is called {nameof(MyClass)}.");
Wynik:
Ciąg ma 27 znaków:
Moja klasa nazywa się MyClass.
Czeka na złapanie i wreszcie
Jest możliwe użycie await
wyraz zastosowania operatora czekają na zadania lub zadania (z TResult) w catch
i finally
bloków w C # 6.
Ze względu na ograniczenia kompilatora nie było możliwe użycie wyrażenia await
w catch
i finally
bloków we wcześniejszych wersjach. C # 6 znacznie ułatwia oczekiwanie na zadania asynchroniczne, umożliwiając wyrażenie 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();
}
W C # 5 wymagane było użycie bool
lub zadeklarowanie Exception
poza try catch, aby wykonać operacje asynchroniczne. Ta metoda jest pokazana w następującym przykładzie:
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();
Propagacja zerowa
?.
operator i operator ?[...]
nazywane są operatorem zerowym . Czasami nazywa się to także innymi nazwami, takimi jak operator bezpiecznej nawigacji .
Jest to przydatne, ponieważ jeśli .
Operator (członek akcesora) jest stosowany do wyrażenia, które ma null
, program zgłosi NullReferenceException
. Jeśli zamiast tego programista używa ?.
(warunkowo-null), wyrażenie wyliczy wartość null zamiast zgłaszać wyjątek.
Zauważ, że jeśli ?.
Operator jest używany i ekspresja nie jest zerowa, ?.
a .
są równoważne.
Podstawy
var teacherName = classroom.GetTeacher().Name;
// throws NullReferenceException if GetTeacher() returns null
Jeśli w classroom
nie ma nauczyciela, GetTeacher()
może zwrócić null
. Gdy ma null
i uzyskuje się dostęp do właściwości Name
, NullReferenceException
jest NullReferenceException
.
Jeśli zmodyfikujemy to oświadczenie, aby użyć ?.
składnia, wynik całego wyrażenia będzie null
:
var teacherName = classroom.GetTeacher()?.Name;
// teacherName is null if GetTeacher() returns null
Następnie, jeśli classroom
mogłaby również być null
, moglibyśmy również napisać to oświadczenie jako:
var teacherName = classroom?.GetTeacher()?.Name;
// teacherName is null if GetTeacher() returns null OR classroom is null
Jest to przykład zwarcia: Gdy jakakolwiek operacja dostępu warunkowego z użyciem operatora zerowo-warunkowego ocenia na zero, całe wyrażenie natychmiast ocenia na zero, bez przetwarzania reszty łańcucha.
Gdy element końcowy wyrażenia zawierającego operator warunkowy o wartości NULL jest typu wartości, wyrażenie to daje wartość Nullable<T>
tego typu, a zatem nie może być użyte jako bezpośrednie zastąpienie wyrażenia bez ?.
.
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
Używaj z operatorem zerowania koalescencji (??)
Możesz połączyć operator warunkowy zerowy z operatorem koalescencyjnym zerowym ( ??
), aby zwrócić wartość domyślną, jeśli wyrażenie ma null
. Korzystając z naszego przykładu powyżej:
var teacherName = classroom?.GetTeacher()?.Name ?? "No Name";
// teacherName will be "No Name" when GetTeacher()
// returns null OR classroom is null OR Name is null
Używaj z indeksatorami
Operator indeksowania zerowego może być używany z indeksatorami :
var firstStudentName = classroom?.Students?[0]?.Name;
W powyższym przykładzie:
- Pierwszy
?.
zapewnia, żeclassroom
nie manull
. - Drugi
?
zapewnia, że cała kolekcjaStudents
nie jestnull
. - Trzeci
?.
po tym, jak indeksator upewni się, że indeksator[0]
nie zwrócił obiektunull
. Należy zauważyć, że ta operacja może nadalIndexOutOfRangeException
.
Używaj z nieważnymi funkcjami
Operator zerowo-warunkowy może być również używany z funkcjami void
. Jednak w tym przypadku instrukcja nie będzie miała wartości null
. To po prostu zapobiegnie wyjątkowi NullReferenceException
.
List<string> list = null;
list?.Add("hi"); // Does not evaluate to null
Użyj z wywołaniem zdarzenia
Zakładając następującą definicję zdarzenia:
private event EventArgs OnCompleted;
Podczas wywoływania zdarzenia, tradycyjnie, najlepszą praktyką jest sprawdzenie, czy zdarzenie ma null
w przypadku braku subskrybentów:
var handler = OnCompleted;
if (handler != null)
{
handler(EventArgs.Empty);
}
Ponieważ wprowadzono operator warunkowy o wartości zerowej, wywołanie można zredukować do jednej linii:
OnCompleted?.Invoke(EventArgs.Empty);
Ograniczenia
Operator nieuwarunkowany generuje wartość, a nie wartość, co oznacza, że nie można jej użyć do przypisania właściwości, subskrypcji zdarzeń itp. Na przykład następujący kod nie będzie działał:
// 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
Uwaga:
int? nameLength = person?.Name.Length; // safe if 'person' is null
to nie to samo co:
int? nameLength = (person?.Name).Length; // avoid this
ponieważ ten pierwszy odpowiada:
int? nameLength = person != null ? (int?)person.Name.Length : null;
a ten ostatni odpowiada:
int? nameLength = (person != null ? person.Name : null).Length;
Pomimo operatora trójskładnikowego ?:
Służy tutaj do wyjaśnienia różnicy między dwoma przypadkami, operatory te nie są równoważne. Można to łatwo zademonstrować za pomocą następującego przykładu:
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;
}
}
}
Które wyjścia:
Propagacja zerowa
Zostałem przeczytany
0
Potrójny
Zostałem przeczytany
Zostałem przeczytany
0
Aby uniknąć wielu wywołań, odpowiednikiem byłoby:
var interimResult = foo.Bar;
Console.WriteLine(interimResult != null ? interimResult.Length : (int?)null);
Ta różnica nieco wyjaśnia, dlaczego operator propagacji zerowej nie jest jeszcze obsługiwany w drzewach wyrażeń.
Używanie typu statycznego
using static [Namespace.Type]
dyrektywy using static [Namespace.Type]
umożliwia importowanie statycznych elementów typów i wartości wyliczeniowych. Metody rozszerzeń są importowane jako metody rozszerzeń (tylko z jednego typu), a nie do zakresu najwyższego poziomu.
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));
}
}
Poprawiona rozdzielczość przeciążenia
Poniższy fragment kodu pokazuje przykład przekazania grupy metod (w przeciwieństwie do lambda), gdy oczekiwany jest delegat. Rozwiązanie problemu przeciążenia rozwiązuje teraz ten problem zamiast wywoływać niejednoznaczny błąd przeciążenia ze względu na zdolność C # 6 do sprawdzenia typu zwracanej metody, która została przekazana.
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;
}
}
Wyniki:
Błąd
błąd CS0121: Wywołanie jest niejednoznaczne między następującymi metodami lub właściwościami: „Program.Overloaded (System.Action)” i „Program.Overloaded (System.Func)”
C # 6 może również dobrze obsługiwać następujący przypadek dokładnego dopasowania wyrażeń lambda, który spowodowałby błąd w 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);
}
}
Drobne zmiany i poprawki błędów
Nawiasy są teraz zabronione wokół nazwanych parametrów. Następujące kompiluje się w C # 5, ale nie w C # 6
Console.WriteLine((value: 23));
Argumenty is
i as
nie mogą być grupami metod. Następujące kompiluje się w C # 5, ale nie w C # 6
var result = "".Any is byte;
Natywny kompilator na to pozwolił (chociaż wyświetlał ostrzeżenie), a nawet nie sprawdził kompatybilności metody rozszerzenia, pozwalając na zwariowane rzeczy, takie jak 1.
1.Any is string
lubIDisposable.Dispose is object
.
Zobacz to odniesienie, aby uzyskać informacje na temat zmian.
Korzystanie z metody rozszerzenia do inicjowania kolekcji
Składnia inicjalizacji kolekcji może być używana podczas tworzenia instancji dowolnej klasy, która implementuje IEnumerable
i ma metodę o nazwie Add
która przyjmuje pojedynczy parametr.
W poprzednich wersjach ta metoda Add
musiała być metodą instancji zainicjowanej klasy. W C # 6 może to być również metoda rozszerzenia.
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
}
}
Spowoduje to wygenerowanie:
Element dodany za pomocą metody dodawania instancji: 1
Element dodany za pomocą metody dodawania instancji: 2
Element dodany za pomocą metody dodawania instancji: 3
Element dodany metodą dodawania rozszerzenia: 4
Element dodany metodą dodawania rozszerzenia: 5
Element dodany metodą dodawania rozszerzenia: 6
Wyłącz ulepszenia ostrzeżeń
W wersji C # 5.0 i wcześniejszych programista mógł ukrywać ostrzeżenia tylko liczbowo. Wraz z wprowadzeniem Roslyn Analyzers, C # potrzebuje sposobu na wyłączenie ostrzeżeń wydawanych z określonych bibliotek. W C # 6.0 dyrektywa pragma może tłumić ostrzeżenia według nazwy.
Przed:
#pragma warning disable 0501
C # 6.0:
#pragma warning disable CS0501