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 nameofnameof 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:

6.0

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.

Zobacz demo


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 klauzuli when , Exception od klauzuli when jest ignorowany i traktowany jako false . Takie podejście umożliwia programistom pisanie klauzuli when 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);
}

Zobacz demo

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;
}

Zobacz demo

Powszechnym podejściem w poprzednich wersjach C # było rejestrowanie i ponowne zgłaszanie wyjątku.

6.0
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);
}

Zobacz demo


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

Zobacz demo

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:

6.0
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,

6.0
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ą

Zobacz demo


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;
}

Zobacz demo


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

Zobacz demo

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.

Zobacz demo

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

Zobacz demo


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

Zobacz demo

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 $

Zobacz demo

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

Zobacz demo

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!

Zobacz demo

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

Zobacz demo


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

Zobacz demo

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

Zobacz demo

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

Zobacz demo

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, że classroom nie ma null .
  • Drugi ? zapewnia, że cała kolekcja Students nie jest null .
  • Trzeci ?. po tym, jak indeksator upewni się, że indeksator [0] nie zwrócił obiektu null . Należy zauważyć, że ta operacja może nadal IndexOutOfRangeException .

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

Zobacz demo

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.

6.0
using static System.Console;
using static System.ConsoleColor;
using static System.Math;

class Program
{
    static void Main()
    {
        BackgroundColor = DarkBlue;
        WriteLine(Sqrt(2));
    }
}

Live Demo Fiddle

6.0
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:

6.0

Wynik

przeciążenie wywołane Func <int>

Zobacz demo

5.0

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

5.0
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

5.0
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 lub IDisposable.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


Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow