C# Language
C # 7.0 Caratteristiche
Ricerca…
introduzione
C # 7.0 è la settima versione di C #. Questa versione contiene alcune nuove funzionalità: supporto linguistico per Tuple, funzioni locali, dichiarazioni out var
, separatori di cifre, valori letterali binari, corrispondenza di pattern, espressioni di lancio, ref return
e ref local
list di membri body con espressioni ref local
ed estese.
Riferimento ufficiale: Novità in C # 7
out var dichiarazione
Un modello comune in C # sta utilizzando bool TryParse(object input, out object value)
per analizzare in modo sicuro gli oggetti.
La dichiarazione out var
è una funzione semplice per migliorare la leggibilità. Permette di dichiarare una variabile nello stesso momento in cui viene passata come parametro out.
Una variabile dichiarata in questo modo è portata al resto del corpo nel punto in cui è dichiarata.
Esempio
Usando TryParse
prima di C # 7.0, devi dichiarare una variabile per ricevere il valore prima di chiamare la funzione:
int value;
if (int.TryParse(input, out value))
{
Foo(value); // ok
}
else
{
Foo(value); // value is zero
}
Foo(value); // ok
In C # 7.0, è possibile allineare la dichiarazione della variabile passata al parametro out
, eliminando la necessità di una dichiarazione di variabile separata:
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
Se alcuni dei parametri che una funzione ritorna in out
non è necessario, è possibile utilizzare l'operatore di scarto _
.
p.GetCoordinates(out var x, out _); // I only care about x
Una dichiarazione out var
può essere utilizzata con qualsiasi funzione esistente che ha già parametri out
. La sintassi della dichiarazione di funzione rimane la stessa e non sono necessari requisiti aggiuntivi per rendere la funzione compatibile con una dichiarazione out var
. Questa caratteristica è semplicemente zucchero sintattico.
Un'altra caratteristica della dichiarazione out var
è che può essere utilizzato con tipi anonimi.
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 questo codice creiamo un Dictionary
con chiave int
e array di valore di tipo anonimo. Nella versione precedente di C # era impossibile utilizzare qui il metodo TryGetValue
poiché richiedeva di dichiarare la variabile out
(che è di tipo anonimo!). Tuttavia, con out var
non è necessario specificare esplicitamente il tipo di variabile out
.
limitazioni
Si noti che le dichiarazioni var out sono di uso limitato nelle query LINQ poiché le espressioni vengono interpretate come corpi lambda espressione, quindi l'ambito delle variabili introdotte è limitato a questi lambda. Ad esempio, il seguente codice non funzionerà:
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
Riferimenti
Letterali binari
Il prefisso 0b può essere utilizzato per rappresentare i valori letterali binari.
I letterali binari consentono di costruire numeri da zero e uno, il che rende molto più facile vedere quali bit sono impostati nella rappresentazione binaria di un numero. Questo può essere utile per lavorare con i flag binari.
I seguenti sono modi equivalenti per specificare un valore int
con il valore 34
(= 2 5 + 2 1 ):
// 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
Elenchi di bandiere
Prima, specificare i valori dei flag per un enum
poteva essere fatto usando uno dei tre metodi in questo esempio:
[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
}
Con i letterali binari è più ovvio quali bit sono impostati e il loro utilizzo non richiede la comprensione di numeri esadecimali e aritmetica bit a bit:
[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
}
Separatori di cifre
Il carattere di sottolineatura _
può essere utilizzato come separatore di cifre. Essere in grado di raggruppare cifre in grandi valori letterali numerici ha un impatto significativo sulla leggibilità.
Il carattere di sottolineatura può comparire ovunque in un valore letterale numerico, ad eccezione di quanto indicato di seguito. Raggruppamenti diversi possono avere senso in diversi scenari o con differenti basi numeriche.
Qualsiasi sequenza di cifre può essere separata da uno o più caratteri di sottolineatura. Il _
è permesso in decimali così come in esponenti. I separatori non hanno alcun impatto semantico: sono semplicemente ignorati.
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;
Dove non è possibile utilizzare il separatore di cifre _
:
- all'inizio del valore (
_121
) - alla fine del valore (
121_
o121.05_
) - accanto al decimale (
10_.0
) - accanto al carattere esponente (
1.1e_1
) - accanto allo specificatore di tipo (
10_f
) - immediatamente dopo il
0x
o0b
in letterali binari ed esadecimali ( potrebbe essere modificato per consentire, ad esempio, 0b_1001_1000 )
Supporto linguistico per Tuples
Nozioni di base
Una tupla è un elenco di elementi ordinato e finito. Le tuple sono comunemente utilizzate nella programmazione come mezzo per lavorare collettivamente con una singola entità invece di lavorare individualmente con ciascuno degli elementi della tupla e per rappresentare singole righe (cioè "record") in un database relazionale.
In C # 7.0, i metodi possono avere più valori di ritorno. Dietro le quinte, il compilatore utilizzerà la nuova struttura ValueTuple .
public (int sum, int count) GetTallies()
{
return (1, 2);
}
Nota a margine : per farlo funzionare in Visual Studio 2017, è necessario ottenere il pacchetto System.ValueTuple
.
Se un risultato del metodo tuple-return è assegnato a una singola variabile, è possibile accedere ai membri con i loro nomi definiti sulla firma del metodo:
var result = GetTallies();
// > result.sum
// 1
// > result.count
// 2
Decuplicazione delle tuple
La decostruzione della tupla separa una tupla nelle sue parti.
Ad esempio, il GetTallies
e l'assegnazione del valore di ritorno a due variabili separate decostruiscono la tupla in queste due variabili:
(int tallyOne, int tallyTwo) = GetTallies();
var
funziona anche:
(var s, var c) = GetTallies();
Puoi anche usare la sintassi più breve, con var
al di fuori di ()
:
var (s, c) = GetTallies();
Puoi anche decostruire in variabili esistenti:
int s, c;
(s, c) = GetTallies();
Lo swapping ora è molto più semplice (non è necessaria alcuna variabile temporanea):
(b, a) = (a, b);
È interessante notare che qualsiasi oggetto può essere decostruito definendo un metodo Deconstruct
nella classe:
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 questo caso, la sintassi della (localFirstName, localLastName) = person
sta invocando Deconstruct
sulla person
.
La decostruzione può anche essere definita in un metodo di estensione. Questo è equivalente a quanto sopra:
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;
Un approccio alternativo per la classe Person
è definire il Name
stesso come una Tuple
. Considera quanto segue:
class Person
{
public (string First, string Last) Name { get; }
public Person((string FirstName, string LastName) name)
{
Name = name;
}
}
Quindi puoi istanziare una persona in questo modo (dove possiamo prendere una tupla come argomento):
var person = new Person(("Jane", "Smith"));
var firstName = person.Name.First; // "Jane"
var lastName = person.Name.Last; // "Smith"
Inizializzazione della tupla
Puoi anche creare arbitrariamente tuple nel codice:
var name = ("John", "Smith");
Console.WriteLine(name.Item1);
// Outputs John
Console.WriteLine(name.Item2);
// Outputs Smith
Quando si crea una tupla, è possibile assegnare nomi di elementi ad hoc ai membri della tupla:
var name = (first: "John", middle: "Q", last: "Smith");
Console.WriteLine(name.first);
// Outputs John
Tipo di inferenza
Più tuple definite con la stessa firma (tipi e conteggi corrispondenti) saranno dedotti come tipi corrispondenti. Per esempio:
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
possono essere restituite poiché la dichiarazione della variabile stats
e la firma di ritorno del metodo sono una corrispondenza.
Nomi di campo di riflessione e tupla
I nomi dei membri non esistono in fase di runtime. Reflection considererà le tuple con lo stesso numero e tipi di membri uguali anche se i nomi dei membri non corrispondono. Convertire una tupla in un object
e poi in una tupla con gli stessi tipi di membri, ma nomi diversi, non causerà nemmeno un'eccezione.
Mentre la classe ValueTuple stessa non conserva le informazioni per i nomi dei membri, le informazioni sono disponibili tramite la riflessione in TupleElementNamesAttribute. Questo attributo non viene applicato alla tupla stessa ma ai parametri del metodo, ai valori restituiti, alle proprietà e ai campi. Ciò consente di preservare i nomi delle tuple attraverso gli assembly, ovvero se un metodo restituisce (nome stringa, conteggio int) il nome e il conteggio dei nomi saranno disponibili ai chiamanti del metodo in un altro assembly poiché il valore restituito sarà contrassegnato con TupleElementNameAttribute che contiene i valori "nome" e "conteggio".
Utilizzare con generici e async
Le nuove funzionalità di tuple (che utilizzano il tipo ValueTuple
sottostante) supportano completamente i generici e possono essere utilizzate come parametri di tipo generico. Ciò rende possibile usarli con il modello async
/ await
:
public async Task<(string value, int count)> GetValueAsync()
{
string fooBar = await _stackoverflow.GetStringAsync();
int num = await _stackoverflow.GetIntAsync();
return (fooBar, num);
}
Utilizzare con le raccolte
Potrebbe essere utile disporre di una raccolta di tuple (ad esempio) in uno scenario in cui si stia tentando di trovare una tupla corrispondente in base alle condizioni per evitare la ramificazione del codice.
Esempio:
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;
}
Con le nuove tuple può diventare:
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;
}
Sebbene la denominazione sopra la tupla di esempio sopra sia piuttosto generica, l'idea di etichette pertinenti consente una comprensione più profonda di ciò che viene tentato nel codice rispetto a "item1", "item2" e "item3".
Differenze tra ValueTuple e Tuple
La ragione principale per l'introduzione di ValueTuple
è la prestazione.
Digita il nome | ValueTuple | Tuple |
---|---|---|
Classe o struttura | struct | class |
Mutabilità (modifica dei valori dopo la creazione) | mutevole | immutabile |
Assegnazione di nomi ai membri e supporto per altre lingue | sì | no ( TBD ) |
Riferimenti
- Proposta di caratteristiche linguistiche originali Tuples su GitHub
- Una soluzione VS 15 percorribile per le funzionalità C # 7.0
- NuGet Tuple Package
Funzioni locali
Le funzioni locali sono definite all'interno di un metodo e non sono disponibili al di fuori di esso. Hanno accesso a tutte le variabili locali e supportano iteratori, async
/ await
e sintassi lambda. In questo modo, le ripetizioni specifiche di una funzione possono essere funzionalizzate senza affollare la classe. Come effetto collaterale, questo migliora le prestazioni di suggerimento intellisense.
Esempio
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;
}
}
Le funzioni locali semplificano considerevolmente il codice per gli operatori LINQ, in cui di solito è necessario separare i controlli degli argomenti dalla logica effettiva per rendere istantanei gli assegni degli argomenti, non ritardati fino a dopo l'avvio dell'iterazione.
Esempio
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;
}
}
Le funzioni locali supportano anche le parole chiave async
e await
.
Esempio
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);
}
}
}
}
Una cosa importante che potresti aver notato è che le funzioni locali possono essere definite sotto l'istruzione return
, non è necessario che siano definite sopra di essa. Inoltre, le funzioni locali in genere seguono la convenzione di denominazione "lowerCamelCase" per distinguersi più facilmente dalle funzioni dell'ambito di classe.
Pattern Matching
Le estensioni di pattern matching per C # abilitano molti dei vantaggi dell'abbinamento di pattern da linguaggi funzionali, ma in un modo che si integra perfettamente con la sensazione del linguaggio sottostante
switch
espressione
Pattern matching estende l' switch
dichiarazione per accendere i tipi:
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
espressione
La corrispondenza del modello estende l'operatore is
per controllare un tipo e dichiarare una nuova variabile allo stesso tempo.
Esempio
string s = o as string;
if(s != null)
{
// do something with s
}
può essere riscritto come:
if(o is string s)
{
//Do something with s
};
Si noti inoltre che l'ambito della variabile di pattern s
viene esteso al di fuori del blocco if
che raggiunge la fine dell'ambito di inclusione, ad esempio:
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 ritorno e ref locale
Ref return e ref locals sono utili per manipolare e restituire riferimenti a blocchi di memoria invece di copiare memoria senza ricorrere a puntatori non sicuri.
Restituzione
public static ref TValue Choose<TValue>(
Func<bool> condition, ref TValue left, ref TValue right)
{
return condition() ? ref left : ref right;
}
Con questo puoi passare due valori per riferimento con uno di essi restituito in base ad alcune condizioni:
Matrix3D left = …, right = …;
Choose(chooser, ref left, ref right).M20 = 1.0;
Ref locale
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
Operazioni di riferimento non sicure
In System.Runtime.CompilerServices.Unsafe
una serie di operazioni non sicure sono stati definiti che permettono di manipolare ref
valori come se fossero puntatori, in fondo.
Ad esempio, reinterpretare un indirizzo di memoria ( ref
) come un tipo diverso:
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]);
Attenzione però a fare questo, ad esempio, controlla BitConverter.IsLittleEndian
se necessario e BitConverter.IsLittleEndian
conseguenza.
Oppure scorrere su un array in modo non sicuro:
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);
O la Subtract
simile:
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);
Inoltre, si può controllare se due ref
valori sono uguali stesso indirizzo cioè:
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]));
link
System.Runtime.CompilerServices.Unsafe su github
lanciare espressioni
C # 7.0 consente di lanciare come espressione in determinati punti:
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();
}
Prima di C # 7.0, se si desidera generare un'eccezione da un corpo di espressione, è necessario:
var spoons = "dinner,desert,soup".Split(',');
var spoonsArray = spoons.Length > 0 ? spoons : null;
if (spoonsArray == null)
{
throw new Exception("There are no spoons");
}
O
var spoonsArray = spoons.Length > 0
? spoons
: new Func<string[]>(() =>
{
throw new Exception("There are no spoons");
})();
In C # 7.0 quanto sopra è ora semplificato per:
var spoonsArray = spoons.Length > 0 ? spoons : throw new Exception("There are no spoons");
Elenco dei membri del corpo con espressione estesa
C # 7.0 aggiunge accessor, costruttori e finalizzatori all'elenco di cose che possono avere corpi di espressioni:
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
}
}
Vedere anche la sezione di dichiarazione out var per l'operatore di scarto.
ValueTask
Task<T>
è una classe e causa l'inutile sovraccarico della sua allocazione quando il risultato è immediatamente disponibile.
ValueTask<T>
è una struttura ed è stato introdotto per impedire l'allocazione di un oggetto Task
nel caso in cui il risultato dell'operazione asincrona sia già disponibile al momento dell'attesa.
Quindi ValueTask<T>
offre due vantaggi:
1. Aumento delle prestazioni
Ecco un esempio di Task<T>
:
- Richiede allocazione dell'heap
- Prende 120ns con JIT
async Task<int> TestTask(int d)
{
await Task.Delay(d);
return 10;
}
Ecco l'esempio analogico ValueTask<T>
:
- No allocazione heap se il risultato è noto in modo sincrono (il che non è in questo caso a causa della
Task.Delay
, ma spesso è in molti nel mondo realeasync
/await
scenari) - Prende 65ns con JIT
async ValueTask<int> TestValueTask(int d)
{
await Task.Delay(d);
return 10;
}
2. Maggiore flessibilità di implementazione
Le implementazioni di un'interfaccia asincrona che desiderano essere sincrone sarebbero altrimenti costrette a utilizzare Task.Run
o Task.FromResult
(con conseguente penalizzazione delle prestazioni discussa sopra). Quindi c'è una certa pressione contro le implementazioni sincrone.
Ma con ValueTask<T>
, le implementazioni sono più libere di scegliere tra l'essere sincrono o asincrono senza impatto sui chiamanti.
Ad esempio, ecco un'interfaccia con un metodo asincrono:
interface IFoo<T>
{
ValueTask<T> BarAsync();
}
... ed ecco come si potrebbe chiamare quel metodo:
IFoo<T> thing = getThing();
var x = await thing.BarAsync();
Con ValueTask
, il codice sopra funzionerà con implementazioni sincrone o asincrone :
Implementazione sincrona:
class SynchronousFoo<T> : IFoo<T>
{
public ValueTask<T> BarAsync()
{
var value = default(T);
return new ValueTask<T>(value);
}
}
Implementazione asincrona
class AsynchronousFoo<T> : IFoo<T>
{
public async ValueTask<T> BarAsync()
{
var value = default(T);
await Task.Delay(1);
return value;
}
}
Gli appunti
Sebbene la struttura ValueTask
fosse stata pianificata per essere aggiunta a C # 7.0 , per ora è stata conservata come un'altra libreria. ValueTask <T> Il pacchetto System.Threading.Tasks.Extensions
può essere scaricato dalla Galleria Nuget