C# Language
C # 7.0-Funktionen
Suche…
Einführung
C # 7.0 ist die siebte Version von C #. Diese Version enthält einige neue Funktionen: Sprachunterstützung für Tupel, lokale Funktionen, out var
Deklarationen, Zifferntrennzeichen, Binärliterale, Musterabgleich, Wurfausdrücke, ref return
und ref local
Liste der ref local
und erweiterten Ausdruckselemente.
Offizielle Referenz: Was ist neu in C # 7?
aus var deklaration
Ein übliches Muster in C # ist die Verwendung von bool TryParse(object input, out object value)
, um Objekte sicher zu analysieren.
Die out var
Deklaration ist eine einfache Funktion zur Verbesserung der Lesbarkeit. Es ermöglicht, dass eine Variable gleichzeitig als deklarierter Parameter deklariert wird.
Eine auf diese Weise deklarierte Variable bezieht sich auf den Rest des Körpers an der Stelle, an der sie deklariert wird.
Beispiel
Bei Verwendung von TryParse
vor C # 7.0 müssen Sie vor dem Aufruf der Funktion eine Variable deklarieren, die den Wert erhalten soll:
int value;
if (int.TryParse(input, out value))
{
Foo(value); // ok
}
else
{
Foo(value); // value is zero
}
Foo(value); // ok
In C # 7.0 können Sie die Deklaration der an den out
Parameter übergebenen Variablen einschließen, sodass keine separate Variablendeklaration erforderlich ist:
if (int.TryParse(input, out var value))
{
Foo(value); // ok
}
else
{
Foo(value); // value is zero
}
Foo(value); // still ok, the value in scope within the remainder of the body
Wenn einige der Parameter, die eine Funktion in out
zurückgibt out
nicht benötigt werden, können Sie den Verwerfungsoperator _
.
p.GetCoordinates(out var x, out _); // I only care about x
Eine out var
Deklaration kann mit jeder vorhandenen Funktion verwendet werden, für die bereits out
Parameter vorhanden sind. Die Funktionsdeklarationssyntax bleibt gleich und es sind keine zusätzlichen Anforderungen erforderlich, um die Funktion mit einer out var
Deklaration kompatibel zu machen. Diese Funktion ist einfach syntaktischer Zucker.
Eine andere Funktion der out var
Deklaration ist, dass sie mit anonymen Typen verwendet werden kann.
var a = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var groupedByMod2 = a.Select(x => new
{
Source = x,
Mod2 = x % 2
})
.GroupBy(x => x.Mod2)
.ToDictionary(g => g.Key, g => g.ToArray());
if (groupedByMod2.TryGetValue(1, out var oddElements))
{
Console.WriteLine(oddElements.Length);
}
In diesem Code erstellen wir ein Dictionary
mit einem int
Schlüssel und einem Array mit einem anonymen Typwert. In der vorherigen Version von C # war es nicht möglich, die TryGetValue
Methode hier zu verwenden, da Sie die out
Variable (die vom anonymen Typ ist!) TryGetValue
. Mit out var
wir jedoch den Typ der out
Variablen nicht explizit angeben.
Einschränkungen
Beachten Sie, dass out-Var-Deklarationen in LINQ-Abfragen nur von begrenztem Nutzen sind, da Ausdrücke als Ausdrucks-Lambda-Körper interpretiert werden. Der Umfang der eingeführten Variablen ist daher auf diese Lambdas beschränkt. Der folgende Code funktioniert beispielsweise nicht:
var nums =
from item in seq
let success = int.TryParse(item, out var tmp)
select success ? tmp : 0; // Error: The name 'tmp' does not exist in the current context
Verweise
Binäre Literale
Das 0b- Präfix kann verwendet werden, um binäre Literale darzustellen.
Binäre Literale ermöglichen das Erstellen von Zahlen aus Nullen und Einsen. Dadurch wird deutlich, welche Bits in der binären Darstellung einer Zahl gesetzt sind. Dies kann nützlich sein, wenn Sie mit binären Flags arbeiten.
Im Folgenden sind gleichwertige Möglichkeiten zum Angeben eines int
mit dem Wert 34
(= 2 5 + 2 1 ) angegeben:
// Using a binary literal:
// bits: 76543210
int a1 = 0b00100010; // binary: explicitly specify bits
// Existing methods:
int a2 = 0x22; // hexadecimal: every digit corresponds to 4 bits
int a3 = 34; // decimal: hard to visualise which bits are set
int a4 = (1 << 5) | (1 << 1); // bitwise arithmetic: combining non-zero bits
Flags Aufzählungen
Das Festlegen von Flagwerten für eine enum
war zuvor nur mit einer der drei Methoden in diesem Beispiel möglich:
[Flags]
public enum DaysOfWeek
{
// Previously available methods:
// decimal hex bit shifting
Monday = 1, // = 0x01 = 1 << 0
Tuesday = 2, // = 0x02 = 1 << 1
Wednesday = 4, // = 0x04 = 1 << 2
Thursday = 8, // = 0x08 = 1 << 3
Friday = 16, // = 0x10 = 1 << 4
Saturday = 32, // = 0x20 = 1 << 5
Sunday = 64, // = 0x40 = 1 << 6
Weekdays = Monday | Tuesday | Wednesday | Thursday | Friday,
Weekends = Saturday | Sunday
}
Bei binären Literalen ist es offensichtlicher, welche Bits gesetzt sind, und deren Verwendung erfordert kein Verständnis der Hexadezimalzahlen und der bitweisen Arithmetik:
[Flags]
public enum DaysOfWeek
{
Monday = 0b00000001,
Tuesday = 0b00000010,
Wednesday = 0b00000100,
Thursday = 0b00001000,
Friday = 0b00010000,
Saturday = 0b00100000,
Sunday = 0b01000000,
Weekdays = Monday | Tuesday | Wednesday | Thursday | Friday,
Weekends = Saturday | Sunday
}
Zifferntrennzeichen
Der Unterstrich _
kann als Zifferntrennzeichen verwendet werden. Die Möglichkeit, Ziffern in großen numerischen Literalen zu gruppieren, hat einen erheblichen Einfluss auf die Lesbarkeit.
Der Unterstrich kann an einer beliebigen Stelle in einem numerischen Literal vorkommen, außer wie unten angegeben. Unterschiedliche Gruppierungen können in verschiedenen Szenarien oder mit unterschiedlichen numerischen Grundlagen sinnvoll sein.
Jede Ziffernfolge kann durch einen oder mehrere Unterstriche getrennt werden. Das _
ist sowohl in Dezimalzahlen als auch in Exponenten zulässig. Die Trennzeichen haben keine semantischen Auswirkungen - sie werden einfach ignoriert.
int bin = 0b1001_1010_0001_0100;
int hex = 0x1b_a0_44_fe;
int dec = 33_554_432;
int weird = 1_2__3___4____5_____6______7_______8________9;
double real = 1_000.111_1e-1_000;
Wenn das _
Trennzeichen nicht verwendet werden darf:
- am Anfang des Wertes (
_121
) - am Ende des Wertes (
121_
oder121.05_
) - neben der Dezimalzahl (
10_.0
) - neben dem Exponentenzeichen (
1.1e_1
) - neben dem Typbezeichner (
10_f
) - unmittelbar nach dem
0x
oder0b
in Binär- und Hexadezimal-Literalen ( kann geändert werden, um beispielsweise 0b_1001_1000 zuzulassen )
Sprachunterstützung für Tupel
Grundlagen
Ein Tupel ist eine geordnete, endliche Liste von Elementen. Tupel werden üblicherweise in der Programmierung als Mittel verwendet, um mit einer einzelnen Entität zusammenzuarbeiten, anstatt einzeln mit jedem der Elemente des Tupels zu arbeiten, und um einzelne Zeilen (dh "Datensätze") in einer relationalen Datenbank darzustellen.
In C # 7.0 können Methoden mehrere Rückgabewerte enthalten. Hinter den Kulissen verwendet der Compiler die neue ValueTuple- Struktur.
public (int sum, int count) GetTallies()
{
return (1, 2);
}
Randbemerkung: für das in Visual Studio 2017 zu arbeiten, müssen Sie die bekommen System.ValueTuple
Paket.
Wenn ein Ergebnis für eine Methode, die ein Tupel zurückgibt, einer einzelnen Variablen zugewiesen wird, können Sie auf die Member über ihre definierten Namen in der Methodensignatur zugreifen:
var result = GetTallies();
// > result.sum
// 1
// > result.count
// 2
Tuple Dekonstruktion
Tupel Dekonstruktion trennt ein Tupel in seine Teile.
Wenn Sie beispielsweise GetTallies
und den Rückgabewert zwei separaten Variablen GetTallies
, dekonstruieren Sie das Tupel in diese beiden Variablen:
(int tallyOne, int tallyTwo) = GetTallies();
var
funktioniert auch:
(var s, var c) = GetTallies();
Sie können auch eine kürzere Syntax verwenden, wobei var
außerhalb von ()
:
var (s, c) = GetTallies();
Sie können auch in vorhandene Variablen zerlegen:
int s, c;
(s, c) = GetTallies();
Das Tauschen ist jetzt viel einfacher (keine temporäre Variable erforderlich):
(b, a) = (a, b);
Interessanterweise kann jedes Objekt dekonstruiert werden, indem eine Deconstruct
Methode in der Klasse definiert wird:
class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public void Deconstruct(out string firstName, out string lastName)
{
firstName = FirstName;
lastName = LastName;
}
}
var person = new Person { FirstName = "John", LastName = "Smith" };
var (localFirstName, localLastName) = person;
In diesem Fall ruft die person
(localFirstName, localLastName) = person
Deconstruct
für die person
.
Dekonstruktion kann sogar in einer Erweiterungsmethode definiert werden. Dies entspricht dem oben genannten:
public static class PersonExtensions
{
public static void Deconstruct(this Person person, out string firstName, out string lastName)
{
firstName = person.FirstName;
lastName = person.LastName;
}
}
var (localFirstName, localLastName) = person;
Ein alternativer Ansatz für die Person
Klasse besteht darin, den Name
selbst als Tuple
zu definieren. Folgendes berücksichtigen:
class Person
{
public (string First, string Last) Name { get; }
public Person((string FirstName, string LastName) name)
{
Name = name;
}
}
Dann können Sie eine Person so instanziieren (wo wir ein Tupel als Argument nehmen können):
var person = new Person(("Jane", "Smith"));
var firstName = person.Name.First; // "Jane"
var lastName = person.Name.Last; // "Smith"
Tupel-Initialisierung
Sie können Tupel auch beliebig im Code erstellen:
var name = ("John", "Smith");
Console.WriteLine(name.Item1);
// Outputs John
Console.WriteLine(name.Item2);
// Outputs Smith
Beim Erstellen eines Tupels können Sie den Mitgliedern des Tupels Ad-hoc-Objektnamen zuweisen:
var name = (first: "John", middle: "Q", last: "Smith");
Console.WriteLine(name.first);
// Outputs John
Inferenz eingeben
Mehrere Tupel, die mit derselben Signatur (übereinstimmende Typen und Anzahl) definiert wurden, werden als übereinstimmende Typen abgeleitet. Zum Beispiel:
public (int sum, double average) Measure(List<int> items)
{
var stats = (sum: 0, average: 0d);
stats.sum = items.Sum();
stats.average = items.Average();
return stats;
}
stats
können zurückgegeben werden, da die Deklaration der Variablen " stats
und die Rückgabesignatur der Methode übereinstimmen.
Reflexions- und Tupelfeldnamen
Mitgliedsnamen sind zur Laufzeit nicht vorhanden. In Reflection werden Tupel mit derselben Anzahl und Elementtypen als identisch betrachtet, auch wenn die Mitgliedsnamen nicht übereinstimmen. Das Konvertieren eines Tupels in ein object
und dann in ein Tupel mit denselben Mitgliedstypen, aber unterschiedlichen Namen, führt ebenfalls nicht zu einer Ausnahme.
Während die ValueTuple-Klasse selbst keine Informationen für Mitgliedsnamen speichert, sind die Informationen durch Reflektion in einem TupleElementNamesAttribute verfügbar. Dieses Attribut wird nicht auf das Tupel selbst angewendet, sondern auf Methodenparameter, Rückgabewerte, Eigenschaften und Felder. Dadurch können die Namen der Tupel-Elemente in allen Assemblys beibehalten werden. Wenn also eine Methode (String-Name, Int-Anzahl) zurückgibt, stehen Name und Anzahl der Namen den Aufrufern der Methode in einer anderen Assembly zur Verfügung, da der Rückgabewert mit TupleElementNameAttribute mit den Werten markiert wird "name" und "count".
Verwendung mit Generika und async
Die neuen Tuple-Features (mit dem zugrunde liegenden ValueTuple
Typ) unterstützen Generics vollständig und können als generische Typparameter verwendet werden. Dadurch ist es möglich, sie mit dem async
/ await
Muster zu verwenden:
public async Task<(string value, int count)> GetValueAsync()
{
string fooBar = await _stackoverflow.GetStringAsync();
int num = await _stackoverflow.GetIntAsync();
return (fooBar, num);
}
Verwenden Sie mit Sammlungen
Es kann von Vorteil sein, eine Sammlung von Tupeln in einem Szenario (als Beispiel) zu haben, in dem Sie versuchen, ein übereinstimmendes Tupel basierend auf Bedingungen zu finden, um eine Verzweigung des Codes zu vermeiden.
Beispiel:
private readonly List<Tuple<string, string, string>> labels = new List<Tuple<string, string, string>>()
{
new Tuple<string, string, string>("test1", "test2", "Value"),
new Tuple<string, string, string>("test1", "test1", "Value2"),
new Tuple<string, string, string>("test2", "test2", "Value3"),
};
public string FindMatchingValue(string firstElement, string secondElement)
{
var result = labels
.Where(w => w.Item1 == firstElement && w.Item2 == secondElement)
.FirstOrDefault();
if (result == null)
throw new ArgumentException("combo not found");
return result.Item3;
}
Mit den neuen Tupeln können:
private readonly List<(string firstThingy, string secondThingyLabel, string foundValue)> labels = new List<(string firstThingy, string secondThingyLabel, string foundValue)>()
{
("test1", "test2", "Value"),
("test1", "test1", "Value2"),
("test2", "test2", "Value3"),
}
public string FindMatchingValue(string firstElement, string secondElement)
{
var result = labels
.Where(w => w.firstThingy == firstElement && w.secondThingyLabel == secondElement)
.FirstOrDefault();
if (result == null)
throw new ArgumentException("combo not found");
return result.foundValue;
}
Obwohl die Benennung im obigen Beispieltupel ziemlich allgemein ist, ermöglicht die Idee der relevanten Labels ein tieferes Verständnis dessen, was im Code versucht wird, wenn auf "item1", "item2" und "item3" verwiesen wird.
Unterschiede zwischen ValueTuple und Tuple
Der Hauptgrund für die Einführung von ValueTuple
ist die Leistung.
Modellname | ValueTuple | Tuple |
---|---|---|
Klasse oder Struktur | struct | class |
Mutabilität (Werte nach Erstellung ändern) | veränderlich | unveränderlich |
Benennen von Mitgliedern und Unterstützung anderer Sprachen | Ja | nein ( TBD ) |
Verweise
- Ursprünglicher Tuples-Sprachfeature-Vorschlag auf GitHub
- Eine lauffähige VS 15-Lösung für C # 7.0-Funktionen
- NuGet Tuple-Paket
Lokale Funktionen
Lokale Funktionen werden innerhalb einer Methode definiert und stehen außerhalb nicht zur Verfügung. Sie haben Zugriff auf alle lokalen Variablen und unterstützen Iteratoren, async
/ await
und Lambda-Syntax. Auf diese Weise können für eine Funktion spezifische Wiederholungen funktionalisiert werden, ohne die Klasse zu überfordern. Als Nebeneffekt verbessert dies die Intellisense-Vorschlagsleistung.
Beispiel
double GetCylinderVolume(double radius, double height)
{
return getVolume();
double getVolume()
{
// You can declare inner-local functions in a local function
double GetCircleArea(double r) => Math.PI * r * r;
// ALL parents' variables are accessible even though parent doesn't have any input.
return GetCircleArea(radius) * height;
}
}
Lokale Funktionen vereinfachen den Code für LINQ-Operatoren erheblich. In diesem Fall müssen Sie normalerweise Argumentprüfungen von der tatsächlichen Logik trennen, um Argumentprüfungen sofort durchzuführen, und nicht verzögert, bis die Iteration gestartet wird.
Beispiel
public static IEnumerable<TSource> Where<TSource>(
this IEnumerable<TSource> source,
Func<TSource, bool> predicate)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (predicate == null) throw new ArgumentNullException(nameof(predicate));
return iterator();
IEnumerable<TSource> iterator()
{
foreach (TSource element in source)
if (predicate(element))
yield return element;
}
}
Lokale Funktionen unterstützen auch die async
und await
Schlüsselwörter.
Beispiel
async Task WriteEmailsAsync()
{
var emailRegex = new Regex(@"(?i)[a-z0-9_.+-]+@[a-z0-9-]+\.[a-z0-9-.]+");
IEnumerable<string> emails1 = await getEmailsFromFileAsync("input1.txt");
IEnumerable<string> emails2 = await getEmailsFromFileAsync("input2.txt");
await writeLinesToFileAsync(emails1.Concat(emails2), "output.txt");
async Task<IEnumerable<string>> getEmailsFromFileAsync(string fileName)
{
string text;
using (StreamReader reader = File.OpenText(fileName))
{
text = await reader.ReadToEndAsync();
}
return from Match emailMatch in emailRegex.Matches(text) select emailMatch.Value;
}
async Task writeLinesToFileAsync(IEnumerable<string> lines, string fileName)
{
using (StreamWriter writer = File.CreateText(fileName))
{
foreach (string line in lines)
{
await writer.WriteLineAsync(line);
}
}
}
}
Eine wichtige Sache, die Sie möglicherweise bemerkt haben, ist, dass lokale Funktionen unter der return
Anweisung definiert werden können. Sie müssen nicht darüber definiert werden. Außerdem folgen lokale Funktionen normalerweise der Namenskonvention "lowerCamelCase", um sich leichter von Klassenbereichsfunktionen zu unterscheiden.
Musterabgleich
Pattern-Matching-Erweiterungen für C # ermöglichen viele Vorteile des Pattern-Matchings aus funktionalen Sprachen, jedoch auf eine Weise, die sich nahtlos in das Gefühl der zugrunde liegenden Sprache einfügt
Ausdruck switch
Pattern Matching erweitert die switch
Anweisung um Typen zu aktivieren:
class Geometry {}
class Triangle : Geometry
{
public int Width { get; set; }
public int Height { get; set; }
public int Base { get; set; }
}
class Rectangle : Geometry
{
public int Width { get; set; }
public int Height { get; set; }
}
class Square : Geometry
{
public int Width { get; set; }
}
public static void PatternMatching()
{
Geometry g = new Square { Width = 5 };
switch (g)
{
case Triangle t:
Console.WriteLine($"{t.Width} {t.Height} {t.Base}");
break;
case Rectangle sq when sq.Width == sq.Height:
Console.WriteLine($"Square rectangle: {sq.Width} {sq.Height}");
break;
case Rectangle r:
Console.WriteLine($"{r.Width} {r.Height}");
break;
case Square s:
Console.WriteLine($"{s.Width}");
break;
default:
Console.WriteLine("<other>");
break;
}
}
is
Ausdruck
Der Musterabgleich erweitert den is
Operator, um nach einem Typ zu suchen und gleichzeitig eine neue Variable zu deklarieren.
Beispiel
string s = o as string;
if(s != null)
{
// do something with s
}
kann umgeschrieben werden als:
if(o is string s)
{
//Do something with s
};
Beachten Sie auch, dass der Gültigkeitsbereich der Mustervariablen s
außerhalb des if
Blocks liegt und das Ende des umschließenden Gültigkeitsbereichs erreicht. Beispiel:
if(someCondition)
{
if(o is string s)
{
//Do something with s
}
else
{
// s is unassigned here, but accessible
}
// s is unassigned here, but accessible
}
// s is not accessible here
ref return und ref local
Ref Returns und Ref-Locals sind nützlich, um Verweise auf Speicherblöcke zu bearbeiten und zurückzugeben, anstatt Speicher zu kopieren, ohne auf unsichere Zeiger zurückzugreifen.
Ref Return
public static ref TValue Choose<TValue>(
Func<bool> condition, ref TValue left, ref TValue right)
{
return condition() ? ref left : ref right;
}
Damit können Sie zwei Werte als Referenz übergeben, wobei einer von ihnen basierend auf einer Bedingung zurückgegeben wird:
Matrix3D left = …, right = …;
Choose(chooser, ref left, ref right).M20 = 1.0;
Ref Local
public static ref int Max(ref int first, ref int second, ref int third)
{
ref int max = first > second ? ref first : ref second;
return max > third ? ref max : ref third;
}
…
int a = 1, b = 2, c = 3;
Max(ref a, ref b, ref c) = 4;
Debug.Assert(a == 1); // true
Debug.Assert(b == 2); // true
Debug.Assert(c == 4); // true
Unsichere Ref-Operationen
In System.Runtime.CompilerServices.Unsafe
ein Satz unsicherer Operationen definiert, mit denen Sie die ref
Werte im Grunde wie Zeiger bearbeiten können.
Zum Beispiel, interpretieren Sie eine Speicheradresse ( ref
) als einen anderen Typ neu:
byte[] b = new byte[4] { 0x42, 0x42, 0x42, 0x42 };
ref int r = ref Unsafe.As<byte, int>(ref b[0]);
Assert.Equal(0x42424242, r);
0x0EF00EF0;
Assert.Equal(0xFE, b[0] | b[1] | b[2] | b[3]);
BitConverter.IsLittleEndian
Sie dabei jedoch auf die Endianness , überprüfen BitConverter.IsLittleEndian
z. B. BitConverter.IsLittleEndian
und behandeln Sie es entsprechend.
Oder durchlaufen Sie ein Array auf unsichere Weise:
int[] a = new int[] { 0x123, 0x234, 0x345, 0x456 };
ref int r1 = ref Unsafe.Add(ref a[0], 1);
Assert.Equal(0x234, r1);
ref int r2 = ref Unsafe.Add(ref r1, 2);
Assert.Equal(0x456, r2);
ref int r3 = ref Unsafe.Add(ref r2, -3);
Assert.Equal(0x123, r3);
Oder das ähnliche Subtract
:
string[] a = new string[] { "abc", "def", "ghi", "jkl" };
ref string r1 = ref Unsafe.Subtract(ref a[0], -2);
Assert.Equal("ghi", r1);
ref string r2 = ref Unsafe.Subtract(ref r1, -1);
Assert.Equal("jkl", r2);
ref string r3 = ref Unsafe.Subtract(ref r2, 3);
Assert.Equal("abc", r3);
Außerdem kann geprüft werden, ob zwei ref
dieselbe Adresse haben, dh dieselbe Adresse:
long[] a = new long[2];
Assert.True(Unsafe.AreSame(ref a[0], ref a[0]));
Assert.False(Unsafe.AreSame(ref a[0], ref a[1]));
Links
System.Runtime.CompilerServices.Unsafe für github
Ausdrücke werfen
C # 7.0 erlaubt das Werfen als Ausdruck an bestimmten Stellen:
class Person
{
public string Name { get; }
public Person(string name) => Name = name ?? throw new ArgumentNullException(nameof(name));
public string GetFirstName()
{
var parts = Name.Split(' ');
return (parts.Length > 0) ? parts[0] : throw new InvalidOperationException("No name!");
}
public string GetLastName() => throw new NotImplementedException();
}
Wenn Sie vor C # 7.0 eine Ausnahme aus einem Ausdruckskörper auslösen möchten, müssen Sie Folgendes tun:
var spoons = "dinner,desert,soup".Split(',');
var spoonsArray = spoons.Length > 0 ? spoons : null;
if (spoonsArray == null)
{
throw new Exception("There are no spoons");
}
Oder
var spoonsArray = spoons.Length > 0
? spoons
: new Func<string[]>(() =>
{
throw new Exception("There are no spoons");
})();
In C # 7.0 ist das Obige nun vereinfacht zu:
var spoonsArray = spoons.Length > 0 ? spoons : throw new Exception("There are no spoons");
Liste der erweiterten Mitglieder mit Ausdruck
C # 7.0 fügt Accessoren, Konstruktoren und Finalisierer der Liste der Dinge hinzu, die Ausdruckskörper haben können:
class Person
{
private static ConcurrentDictionary<int, string> names = new ConcurrentDictionary<int, string>();
private int id = GetId();
public Person(string name) => names.TryAdd(id, name); // constructors
~Person() => names.TryRemove(id, out _); // finalizers
public string Name
{
get => names[id]; // getters
set => names[id] = value; // setters
}
}
Siehe auch den Abschnitt out var-Deklaration für den Discard-Operator.
ValueTask
Task<T>
ist eine Klasse und verursacht den unnötigen Overhead ihrer Zuordnung, wenn das Ergebnis sofort verfügbar ist.
ValueTask<T>
ist eine Struktur und wurde eingeführt, um die Zuweisung eines Task
Objekts zu verhindern, falls das Ergebnis der asynchronen Operation zum Zeitpunkt des Warten bereits verfügbar ist.
ValueTask<T>
bietet also zwei Vorteile:
1. Leistungssteigerung
Hier ist ein Task<T>
Beispiel:
- Erfordert eine Heapzuordnung
- Nimmt 120s mit JIT
async Task<int> TestTask(int d)
{
await Task.Delay(d);
return 10;
}
Hier ist das analoge ValueTask<T>
Beispiel:
- Keine Heapzuordnung , wenn das Ergebnis synchron bekannt ist (was es wegen des in diesem Fall nicht ist
Task.Delay
, ist aber oft in vielen realenasync
/await
Szenarien) - Nimmt 65s mit JIT
async ValueTask<int> TestValueTask(int d)
{
await Task.Delay(d);
return 10;
}
2. Erhöhte Flexibilität bei der Implementierung
Implementierungen einer asynchronen Schnittstelle, die synchron sein wollen, würden sonst gezwungen sein, entweder Task.Run
oder Task.FromResult
(was zu dem oben beschriebenen Performance- Task.FromResult
führt). Daher besteht ein gewisser Druck gegenüber synchronen Implementierungen.
Mit ValueTask<T>
können Implementierungen jedoch mehr frei wählen, ob sie synchron oder asynchron sind, ohne dass Anrufer beeinträchtigt werden.
Hier ist zum Beispiel eine Schnittstelle mit einer asynchronen Methode:
interface IFoo<T>
{
ValueTask<T> BarAsync();
}
... und so könnte man diese Methode nennen:
IFoo<T> thing = getThing();
var x = await thing.BarAsync();
Mit ValueTask
der obige Code entweder mit synchronen oder asynchronen Implementierungen :
Synchrone Implementierung:
class SynchronousFoo<T> : IFoo<T>
{
public ValueTask<T> BarAsync()
{
var value = default(T);
return new ValueTask<T>(value);
}
}
Asynchrone Implementierung
class AsynchronousFoo<T> : IFoo<T>
{
public async ValueTask<T> BarAsync()
{
var value = default(T);
await Task.Delay(1);
return value;
}
}
Anmerkungen
Obwohl ValueTask
wurde, dass ValueTask
Struktur zu C # 7.0 hinzugefügt wurde, wurde sie vorerst als andere Bibliothek beibehalten. Das ValueTask <T> System.Threading.Tasks.Extensions
Paket kann von der Nuget Gallery heruntergeladen werden