Co nowego w języku C# 8.0
W języku C# 8.0 dodano następujące funkcje i ulepszenia języka C#:
- Elementy członkowskie tylko do odczytu
- Domyślne metody interfejsu
- Ulepszenia dopasowania wzorca:
- Używanie deklaracji
- Statyczne funkcje lokalne
- Jednorazowe struktury ref
- Typy referencyjne dopuszczające wartość null
- Strumienie asynchroniczne
- Asynchroniczny jednorazowy
- Indeksy i zakresy
- Przypisanie z wartością ujedną o wartości null
- Niezamanageowane typy konstruowane
- Stackalloc w wyrażeniach zagnieżdżonych
- Ulepszenie interpolowanych ciągów dosłownych
Język C# 8.0 jest obsługiwany na . NET Core 3.x i .NET Standard 2.1. Aby uzyskać więcej informacji, zobacz C# language versioning (Wersje języka C#).
W pozostałej części tego artykułu krótko opisano te funkcje. Tam, gdzie są dostępne szczegółowe artykuły, dostępne są linki do tych samouczków i przeglądów. Możesz eksplorować te funkcje w swoim środowisku przy użyciu narzędzia dotnet try globalnego:
- Zainstaluj narzędzie globalne dotnet-try .
- Sklonuj repozytorium dotnet/try-samples.
- Ustaw bieżący katalog na podkatalog csharp8 dla repozytorium try-samples .
- Uruchom polecenie
dotnet try.
Elementy członkowskie tylko do odczytu
Modyfikator można readonly zastosować do elementów członkowskich struktury. Wskazuje, że członek nie modyfikuje stanu. Jest bardziej szczegółowe niż zastosowanie readonly modyfikatora do deklaracji struct . Rozważmy następującą modyfikowalna struktura:
public struct Point
{
public double X { get; set; }
public double Y { get; set; }
public double Distance => Math.Sqrt(X * X + Y * Y);
public override string ToString() =>
$"({X}, {Y}) is {Distance} from the origin";
}
Podobnie jak w przypadku większości struktur, ToString() metoda nie modyfikuje stanu. Możesz wskazać, że dodając readonly modyfikator do deklaracji :ToString()
public readonly override string ToString() =>
$"({X}, {Y}) is {Distance} from the origin";
Poprzednia zmiana generuje ostrzeżenie kompilatora, ponieważ uzyskuje ToString dostęp do Distance właściwości , która nie jest oznaczona jako readonly:
warning CS8656: Call to non-readonly member 'Point.Distance.get' from a 'readonly' member results in an implicit copy of 'this'
Kompilator ostrzega, gdy konieczne jest utworzenie kopii obronnej. Właściwość Distance nie zmienia stanu, więc można naprawić to ostrzeżenie, dodając readonly modyfikator do deklaracji:
public readonly double Distance => Math.Sqrt(X * X + Y * Y);
Zwróć uwagę, że readonly modyfikator jest niezbędny we właściwości tylko do odczytu. Kompilator nie zakłada, że opcje get dostępu nie modyfikują stanu; należy zadeklarować jawnie readonly . Właściwości zaimplementowane automatycznie są wyjątkiem. Kompilator traktuje wszystkie automatycznie zaimplementowane gettery readonlyjako , readonly więc nie ma potrzeby dodawania modyfikatora do właściwości X i Y .
Kompilator wymusza regułę, która nie readonly modyfikuje stanu przez elementy członkowskie. Następująca metoda nie zostanie skompilowana, chyba że usuniesz readonly modyfikator:
public readonly void Translate(int xOffset, int yOffset)
{
X += xOffset;
Y += yOffset;
}
Ta funkcja umożliwia określenie intencji projektu, dzięki czemu kompilator może ją wymusić, i dokonać optymalizacji na podstawie tej intencji.
Aby uzyskać więcej informacji, zobacz sekcję elementów członkowskich wystąpienia w artykule Typy struktur.
Domyślne metody interfejsu
Teraz można dodawać elementy członkowskie do interfejsów i zapewniać implementację dla tych elementów członkowskich. Ta funkcja języka umożliwia autorom interfejsów API dodawanie metod do interfejsu w nowszych wersjach bez przerywania zgodności źródłowej lub binarnej z istniejącymi implementacjami tego interfejsu. Istniejące implementacje dziedziczą implementację domyślną. Ta funkcja umożliwia również językowi C# współdziałanie z interfejsami API dla systemu Android lub swift, które obsługują podobne funkcje. Domyślne metody interfejsu umożliwiają również scenariusze podobne do funkcji języka "traits".
Domyślne metody interfejsu mają wpływ na wiele scenariuszy i elementów języka. Nasz pierwszy samouczek obejmuje aktualizowanie interfejsu przy użyciu implementacji domyślnych.
Więcej wzorców w większej liczby miejscach
Dopasowywanie wzorca udostępnia narzędzia do zapewnienia funkcji zależnych od kształtu dla powiązanych, ale różnych rodzajów danych. W języku C# 7.0 wprowadzono składnię wzorców typów i wzorców stałych przy użyciu wyrażenia is i switch instrukcji . Te funkcje reprezentowały pierwsze wstępne kroki w kierunku wspierania paradygmatów programowania, w których dane i funkcje są od siebie od siebie odsełowane. W związku z tym, że branża przechodzi w kierunku większej liczby mikrousług i innych architektur opartych na chmurze, potrzebne są inne narzędzia językowe.
Język C# 8.0 rozszerza to słownictwo, dzięki czemu można używać więcej wyrażeń wzorców w większej liczby miejscach w kodzie. Należy wziąć pod uwagę te funkcje, gdy dane i funkcje są oddzielone. Rozważ dopasowanie wzorca, gdy algorytmy zależą od faktu innego niż typ środowiska uruchomieniowego obiektu. Te techniki zapewniają inny sposób wyrażania projektów.
Oprócz nowych wzorców w nowych miejscach język C# 8.0 dodaje wzorce cykliczne. Wzorce cykliczne to wzorce, które mogą zawierać inne wzorce.
Przełączanie wyrażeń
Często instrukcja generuje switch wartość w każdym z jej bloków case . Wyrażenia switch umożliwiają korzystanie z bardziej zwięzłej składni wyrażeń. Istnieje mniej powtarzających się słów case kluczowych i break oraz mniej nawiasów klamrowych. Rozważmy na przykład następujące wyliczanie, które zawiera listę kolorów irysa:
public enum Rainbow
{
Red,
Orange,
Yellow,
Green,
Blue,
Indigo,
Violet
}
Jeśli aplikacja zdefiniuje RGBColor typ R, który jest zbudowany ze składników , G i B , Rainbow można przekonwertować wartość na jej wartości RGB przy użyciu następującej metody zawierającej wyrażenie switch:
public static RGBColor FromRainbow(Rainbow colorBand) =>
colorBand switch
{
Rainbow.Red => new RGBColor(0xFF, 0x00, 0x00),
Rainbow.Orange => new RGBColor(0xFF, 0x7F, 0x00),
Rainbow.Yellow => new RGBColor(0xFF, 0xFF, 0x00),
Rainbow.Green => new RGBColor(0x00, 0xFF, 0x00),
Rainbow.Blue => new RGBColor(0x00, 0x00, 0xFF),
Rainbow.Indigo => new RGBColor(0x4B, 0x00, 0x82),
Rainbow.Violet => new RGBColor(0x94, 0x00, 0xD3),
_ => throw new ArgumentException(message: "invalid enum value", paramName: nameof(colorBand)),
};
W tym miejscu wprowadzono kilka ulepszeń składni:
- Zmienna znajduje się przed słowem kluczowym
switch. Inna kolejność ułatwia wizualne odróżnienie wyrażenia switch od instrukcji switch. - Elementy
casei:są zastępowane elementami=>. Jest to bardziej zwięzłe i intuicyjne. - Przypadek
defaultjest zastępowany odrzuceniem_. - Treści to wyrażenia, a nie instrukcje.
Porównaj to z równoważnym kodem przy użyciu instrukcji klasycznej switch :
public static RGBColor FromRainbowClassic(Rainbow colorBand)
{
switch (colorBand)
{
case Rainbow.Red:
return new RGBColor(0xFF, 0x00, 0x00);
case Rainbow.Orange:
return new RGBColor(0xFF, 0x7F, 0x00);
case Rainbow.Yellow:
return new RGBColor(0xFF, 0xFF, 0x00);
case Rainbow.Green:
return new RGBColor(0x00, 0xFF, 0x00);
case Rainbow.Blue:
return new RGBColor(0x00, 0x00, 0xFF);
case Rainbow.Indigo:
return new RGBColor(0x4B, 0x00, 0x82);
case Rainbow.Violet:
return new RGBColor(0x94, 0x00, 0xD3);
default:
throw new ArgumentException(message: "invalid enum value", paramName: nameof(colorBand));
};
}
Aby uzyskać więcej informacji, zobacz wyrażenie.
Wzorce właściwości
Wzorzec właściwości umożliwia dopasowanie właściwości badanego obiektu. Rozważ witrynę handlu elektronicznego, która musi obliczyć podatek od sprzedaży na podstawie adresu nabywcy. To obliczenie nie jest podstawową odpowiedzialnością Address klasy. Będzie ona zmieniana w czasie, prawdopodobnie częściej niż zmiany formatu. Kwota podatku od sprzedaży zależy od State właściwości adresu. W poniższej metodzie wzorzec właściwości jest używany do obliczenia podatku od sprzedaży na przykładzie adresu i ceny:
public static decimal ComputeSalesTax(Address location, decimal salePrice) =>
location switch
{
{ State: "WA" } => salePrice * 0.06M,
{ State: "MN" } => salePrice * 0.075M,
{ State: "MI" } => salePrice * 0.05M,
// other cases removed for brevity...
_ => 0M
};
Dopasowanie wzorca tworzy zwięzłą składnię do wyrażania tego algorytmu.
Aby uzyskać więcej informacji, zobacz sekcję Wzorzec właściwości w artykule Patterns (Wzorce ).
Wzorce krotki
Niektóre algorytmy zależą od wielu danych wejściowych. Wzorce krotki umożliwiają przełączanie na podstawie wielu wartości wyrażonych jako krotka. Poniższy kod przedstawia wyrażenie przełącznika dla skały gry , papieru, nożyczek:
public static string RockPaperScissors(string first, string second)
=> (first, second) switch
{
("rock", "paper") => "rock is covered by paper. Paper wins.",
("rock", "scissors") => "rock breaks scissors. Rock wins.",
("paper", "rock") => "paper covers rock. Paper wins.",
("paper", "scissors") => "paper is cut by scissors. Scissors wins.",
("scissors", "rock") => "scissors is broken by rock. Rock wins.",
("scissors", "paper") => "scissors cuts paper. Scissors wins.",
(_, _) => "tie"
};
Komunikaty wskazują zwycięzcę. Przypadek odrzucenia reprezentuje trzy kombinacje dla wartości ties lub innych danych wejściowych tekstu.
Wzorce pozyacyjne
Niektóre typy obejmują metodę Deconstruct , która dekonstruuje jej właściwości na zmienne dyskretne. Gdy metoda Deconstruct jest dostępna, można Deconstruct pozytalnych, aby sprawdzić właściwości obiektu i użyć tych właściwości dla wzorca. Rozważmy następującą Point klasę, która zawiera Deconstruct metodę tworzenia dyskretnych zmiennych dla elementów X i Y:
public class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
public void Deconstruct(out int x, out int y) =>
(x, y) = (X, Y);
}
Ponadto rozważmy następujące wylicie, które reprezentuje różne pozycje ćwiartki:
public enum Quadrant
{
Unknown,
Origin,
One,
Two,
Three,
Four,
OnBorder
}
W poniższej metodzie do wyodrębnienia wartości i jest używany wzorzec pozycyjny y. Następnie używa klauzuli when , aby określić Quadrant punkt:
static Quadrant GetQuadrant(Point point) => point switch
{
(0, 0) => Quadrant.Origin,
var (x, y) when x > 0 && y > 0 => Quadrant.One,
var (x, y) when x < 0 && y > 0 => Quadrant.Two,
var (x, y) when x < 0 && y < 0 => Quadrant.Three,
var (x, y) when x > 0 && y < 0 => Quadrant.Four,
var (_, _) => Quadrant.OnBorder,
_ => Quadrant.Unknown
};
Wzorzec odrzucania w poprzednim przełączniku pasuje, xy gdy albo lub jest 0, ale nie oba. Wyrażenie switch musi tworzyć wartość lub zgłaszać wyjątek. Jeśli żaden z przypadków nie pasuje, wyrażenie switch zgłasza wyjątek. Kompilator generuje ostrzeżenie, jeśli nie obejmuje wszystkich możliwych przypadków w wyrażeniu switch.
Techniki dopasowywania wzorców można eksplorować w tym zaawansowanym samouczku na temat dopasowywania wzorców. Aby uzyskać więcej informacji o wzorcu pozycie, zobacz sekcję Wzorzec pozycyjny artykułu Patterns ( Wzorce).
Używanie deklaracji
Deklaracja using jest deklaracją zmiennej poprzedzoną słowem kluczowym . Informuje kompilator, że zadeklarowana zmienna powinna być usuwana na końcu otaczającego zakresu. Rozważmy na przykład następujący kod, który zapisuje plik tekstowy:
static int WriteLinesToFile(IEnumerable<string> lines)
{
using var file = new System.IO.StreamWriter("WriteLines2.txt");
int skippedLines = 0;
foreach (string line in lines)
{
if (!line.Contains("Second"))
{
file.WriteLine(line);
}
else
{
skippedLines++;
}
}
// Notice how skippedLines is in scope here.
return skippedLines;
// file is disposed here
}
W poprzednim przykładzie plik jest usuwany po osiągnięciu zamykającego nawiasu klamrowego dla metody. Jest to koniec zakresu, w którym jest file zadeklarowany. Poprzedni kod jest odpowiednikiem następującego kodu, który używa klasycznej instrukcji using:
static int WriteLinesToFile(IEnumerable<string> lines)
{
using (var file = new System.IO.StreamWriter("WriteLines2.txt"))
{
int skippedLines = 0;
foreach (string line in lines)
{
if (!line.Contains("Second"))
{
file.WriteLine(line);
}
else
{
skippedLines++;
}
}
return skippedLines;
} // file is disposed here
}
W poprzednim przykładzie plik jest usuwany po osiągnięciu zamykającego nawiasu klamrowego skojarzonego z using instrukcji .
W obu przypadkach kompilator generuje wywołanie .Dispose() Kompilator generuje błąd, jeśli wyrażenie w instrukcji using nie jest jednorazowe.
Statyczne funkcje lokalne
Teraz można dodać modyfikator static do funkcji static lokalnych, aby upewnić się, że funkcja lokalna nie przechwyci żadnych zmiennych z otaczającego zakresu (odwołanie). W ten sposób generowany jest CS8421tekst "Statyczna funkcja lokalna nie może zawierać odwołania do <zmiennej>".
Spójrzmy na poniższy kod. Funkcja lokalna LocalFunction uzyskuje dostęp do zmiennej yzadeklarowanej w otaczającym zakresie (metodzie M). W związku LocalFunction z tym nie można zadeklarować za pomocą static modyfikatora :
int M()
{
int y;
LocalFunction();
return y;
void LocalFunction() => y = 0;
}
Poniższy kod zawiera statyczną funkcję lokalną. Może być statyczny, ponieważ nie ma dostępu do żadnych zmiennych w otaczającym zakresie:
int M()
{
int y = 5;
int x = 7;
return Add(x, y);
static int Add(int left, int right) => left + right;
}
Jednorazowe struktury ref
Zadeklarowany struct za pomocą modyfikatora ref może nie implementować żadnych interfejsów, dlatego nie może implementować klasy IDisposable. W związku z tym, aby ref struct umożliwić likwidować element , musi mieć dostępną void Dispose() metodę . Ta funkcja ma również zastosowanie do deklaracji readonly ref struct .
Typy referencyjne dopuszczające wartość null
W kontekście adnotacji dopuszczania wartości null każda zmienna typu referencyjnego jest uważana za typ referencyjny, który nie może być anulowany. Jeśli chcesz wskazać, że zmienna może mieć wartość null, ? musisz dołączyć nazwę typu z wartością , aby zadeklarować zmienną jako typ referencyjny ?.
W przypadku nienadmiernych typów referencyjnych kompilator używa analizy przepływu, aby upewnić się, że zmienne lokalne są inicjowane do wartości innych niż null, gdy są zadeklarowane. Pola muszą zostać zainicjowane podczas budowy. Kompilator generuje ostrzeżenie, jeśli zmienna nie jest ustawiona przez wywołanie żadnego z dostępnych konstruktorów lub inicjatora. Ponadto nienadymierne typy referencyjne nie mogą mieć przypisanej wartości, która może mieć wartość null.
Typy referencyjne dopuszczane wartością null nie są sprawdzane w celu upewnienia się, że nie są przypisane ani zainicjowane do wartości null. Jednak kompilator używa analizy przepływu, aby upewnić się, że każda zmienna typu referencyjnego dopuszczaca wartość null jest sprawdzana pod kątem wartości null przed dostępem do typu referencyjnego lub przypisaniem do niego.
Więcej informacji na temat tej funkcji można znaleźć w przeglądzie typów referencyjnych dopuszczanych do wartości null. Wypróbuj to samodzielnie w nowej aplikacji w tym samouczku dla typów referencyjnych dopuszczania wartości null. Dowiedz się więcej na temat kroków migracji istniejącej bazy kodu w celu używania typów referencyjnych dopuszczanych wartością null w artykule na temat uaktualniania do typów referencyjnych dopuszczanych do wartości null.
Strumienie asynchroniczne
Począwszy od języka C# 8.0, strumienie można tworzyć i używać asynchronicznie. Metoda, która zwraca strumień asynchroniczny, ma trzy właściwości:
- Jest ona zadeklarowana za pomocą
asyncmodyfikatora . - Zwraca wartość IAsyncEnumerable<T>.
- Metoda zawiera
yield returninstrukcje zwracania kolejnych elementów w strumieniu asynchronicznym.
Korzystanie ze strumienia asynchronicznego wymaga dodania słowa kluczowego await przed foreach słowem kluczowym podczas wyliczania elementów strumienia. Dodanie słowa await kluczowego wymaga, aby async metoda wyliczała strumień asynchroniczny z modyfikatorem i zwracała typ dozwolony dla async metody. Zazwyczaj oznacza to zwrócenie lub TaskTask<TResult>. Może to być również lub ValueTaskValueTask<TResult>. Metoda może zarówno używać, jak i tworzyć strumień asynchroniczny, co oznacza, że zwróci ona .IAsyncEnumerable<T> Poniższy kod generuje sekwencję od 0 do 19, oczekując 100 ms między wygenerowaniem każdej liczby:
public static async System.Collections.Generic.IAsyncEnumerable<int> GenerateSequence()
{
for (int i = 0; i < 20; i++)
{
await Task.Delay(100);
yield return i;
}
}
Sekwencję należy wyliczyć przy użyciu instrukcji await foreach :
await foreach (var number in GenerateSequence())
{
Console.WriteLine(number);
}
Możesz samodzielnie wypróbować strumienie asynchroniczne w naszym samouczku na temat tworzenia i zużywania strumieni asynchronicznych. Domyślnie elementy strumienia są przetwarzane w przechwyconym kontekście. Jeśli chcesz wyłączyć przechwytywanie kontekstu, użyj metody TaskAsyncEnumerableExtensions.ConfigureAwait rozszerzenia . Aby uzyskać więcej informacji na temat kontekstów synchronizacji i przechwytywania bieżącego kontekstu, zobacz artykuł na temat korzystania ze wzorca asynchronicznego opartego na zadaniach.
Asynchroniczny jednorazowy
Począwszy od języka C# 8.0, język obsługuje asynchroniczne typy jednorazowe, które implementują System.IAsyncDisposable interfejs. Instrukcja umożliwia pracę await using z asynchronicznie jednorazowym obiektem. Aby uzyskać więcej informacji, zobacz artykuł Implementowanie metody DisposeAsync .
Indeksy i zakresy
Indeksy i zakresy zapewniają zwięzłą składnię do uzyskiwania dostępu do pojedynczych elementów lub zakresów w sekwencji.
Obsługa tego języka opiera się na dwóch nowych typach i dwóch nowych operatorach:
- System.Index element reprezentuje indeks w sekwencję.
- Indeks z operatora końcowego
^, który określa, że indeks jest względny względem końca sekwencji. - System.Range reprezentuje zakres podrzędny sekwencji.
- Operator zakresu
.., który określa początek i koniec zakresu jako operandy.
Zacznijmy od reguł dotyczących indeksów. Rozważmy tablicę sequence. Indeks 0 jest taki sam jak indeks sequence[0]. Indeks ^0 jest taki sam jak indeks sequence[sequence.Length]. Należy pamiętać sequence[^0] , że zgłasza wyjątek, podobnie jak sequence[sequence.Length] w przypadku. Dla dowolnej liczby nindeks jest ^n taki sam jak sequence.Length - n.
Zakres określa począteki koniec zakresu. Początek zakresu jest inkluzywny, ale koniec zakresu jest wyłączny, co oznacza, że początek jest uwzględniony w zakresie, ale koniec nie jest uwzględniony w zakresie. Zakres reprezentuje [0..^0] cały zakres, podobnie jak [0..sequence.Length] cały zakres.
Oto kilka przykładów. Rozważmy następującą tablicę z adnotacjami z indeksem od początku i od końca:
var words = new string[]
{
// index from start index from end
"The", // 0 ^9
"quick", // 1 ^8
"brown", // 2 ^7
"fox", // 3 ^6
"jumped", // 4 ^5
"over", // 5 ^4
"the", // 6 ^3
"lazy", // 7 ^2
"dog" // 8 ^1
}; // 9 (or words.Length) ^0
Możesz pobrać ostatnie słowo z indeksem ^1 :
Console.WriteLine($"The last word is {words[^1]}");
// writes "dog"
Poniższy kod tworzy podzesieć ze słowami "quick", "brown" i "fox". Obejmuje on za words[1] pośrednictwem .words[3] Element nie words[4] znajduje się w zakresie.
var quickBrownFox = words[1..4];
Poniższy kod tworzy podgałęzie z "z opóźnieniem" i "psem". Zawiera on elementy words[^2] i words[^1]. Indeks końcowy nie words[^0] jest uwzględniony:
var lazyDog = words[^2..^0];
Poniższe przykłady tworzą zakresy, które są otwarte dla początku, końca lub obu:
var allWords = words[..]; // contains "The" through "dog".
var firstPhrase = words[..4]; // contains "The" through "fox"
var lastPhrase = words[6..]; // contains "the", "lazy" and "dog"
Zakresy można również zadeklarować jako zmienne:
Range phrase = 1..4;
Zakres może być następnie używany wewnątrz znaków [ i ] :
var text = words[phrase];
Nie tylko tablice obsługują indeksy i zakresy. Można również używać indeksów i zakresów z ciągiem , lub ReadOnlySpan<T>. Aby uzyskać więcej informacji, zobacz Obsługa typów dla indeksów i zakresów.
Więcej informacji na temat indeksów i zakresów można znaleźć w samouczku na temat indeksów i zakresów.
Przypisanie z łańcuszką wartości null
W języku C# 8.0 wprowadzono operator przypisania, który zawiera ??=wartość null. Można użyć operatora ??= , aby przypisać wartość swojego operandu po prawej stronie do jego operandu po lewej stronie tylko wtedy, gdy operand po lewej stronie ma wartość null.
List<int> numbers = null;
int? i = null;
numbers ??= new List<int>();
numbers.Add(i ??= 17);
numbers.Add(i ??= 20);
Console.WriteLine(string.Join(" ", numbers)); // output: 17 17
Console.WriteLine(i); // output: 17
Aby uzyskać więcej informacji, zobacz ?? i ?? = artykuł operatory .
Typy niezamanagedowane skonstruowane
W języku C# w wersji 7.3 lub starszej skonstruowany typ (typ, który zawiera co najmniej jeden argument typu) nie może być typem niezawiązanych. Począwszy od języka C# 8.0, konstruowany typ wartości nie jest niezadawansowany, jeśli zawiera tylko pola typów niezaiemianych.
Na przykład, biorąc pod uwagę następującą definicję typu ogólnego Coords<T> :
public struct Coords<T>
{
public T X;
public T Y;
}
Typ Coords<int> jest typem niezamanagedowany w języku C# 8.0 lub nowszym. Podobnie jak w przypadku dowolnego typu nieza pomocą usługi , można utworzyć wskaźnik do zmiennej tego typu lub przydzielić blok pamięci na stosie dla wystąpień tego typu:
Span<Coords<int>> coordinates = stackalloc[]
{
new Coords<int> { X = 0, Y = 0 },
new Coords<int> { X = 0, Y = 3 },
new Coords<int> { X = 4, Y = 0 }
};
Aby uzyskać więcej informacji, zobacz Typy niezamażowane.
Stackalloc w wyrażeniach zagnieżdżonych
Począwszy od języka C# 8.0, jeśli wynikiem wyrażenia stackalloc jest typ lub System.ReadOnlySpan<T> , stackalloc możesz użyć wyrażenia w innych wyrażeniach:
Span<int> numbers = stackalloc[] { 1, 2, 3, 4, 5, 6 };
var ind = numbers.IndexOfAny(stackalloc[] { 2, 4, 6, 8 });
Console.WriteLine(ind); // output: 1
Ulepszenie ciągów dosłownych interpolowanych
Kolejność tokenów $ i w @$ może być dowolna: zarówno $@"..."@$"..." , jak i są prawidłowymi ciągami interpolowanych dosłownych. We wcześniejszych wersjach języka $ C# token musi pojawić się przed @ tokenem.