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:

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

7.0
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.

7.0
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 _ :

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 no ( TBD )

Riferimenti

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

7.0
string s = o as string;
if(s != null)
{
    // do something with s
}

può essere riscritto come:

7.0
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]));

Roslyn Github Issue

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 reale async / 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



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow