Buscar..


Introducción

C # 7.0 es la séptima versión de C #. Esta versión contiene algunas características nuevas: soporte de idioma para tuplas, funciones locales, declaraciones de out var , separadores de dígitos, literales binarios, coincidencia de patrones, expresiones de lanzamiento, ref return ref local y ref local . Lista de miembros con cuerpo ref local y de expresión extendida.

Referencia oficial: Novedades en C # 7

declaración de var

Un patrón común en C # es usar bool TryParse(object input, out object value) para analizar objetos de forma segura.

La declaración out var es una característica simple para mejorar la legibilidad. Permite que una variable se declare al mismo tiempo que se pasa como un parámetro de salida.

Una variable declarada de esta manera tiene el alcance del resto del cuerpo en el punto en el que se declara.

Ejemplo

Al usar TryParse antes de C # 7.0, debe declarar una variable para recibir el valor antes de llamar a la función:

7.0
int value;
if (int.TryParse(input, out value)) 
{
    Foo(value); // ok
}
else
{
    Foo(value); // value is zero
}

Foo(value); // ok

En C # 7.0, puede alinear la declaración de la variable pasada al parámetro out , eliminando la necesidad de una declaración de variable separada:

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

Si algunos de los parámetros que devuelve una función de out no se necesita se puede utilizar el operador de descartes _ .

p.GetCoordinates(out var x, out _); // I only care about x

Un out var declaración se puede utilizar con cualquier función existente que ya tiene out parámetros. La sintaxis de la declaración de la función sigue siendo la misma, y ​​no se necesitan requisitos adicionales para que la función sea compatible con una declaración de out var . Esta característica es simplemente el azúcar sintáctico.

Otra característica de out var declaración de out var es que puede usarse con tipos anónimos.

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

En este código creamos un Dictionary con clave int y una matriz de valor de tipo anónimo. En la versión anterior de C # era imposible usar el método TryGetValue aquí ya que requería que declararas la variable de out (¡que es de tipo anónimo!). Sin embargo, con out var no necesitamos especificar explícitamente el tipo de la variable out .

Limitaciones

Tenga en cuenta que las declaraciones var son de uso limitado en las consultas LINQ, ya que las expresiones se interpretan como cuerpos lambda de expresión, por lo que el alcance de las variables introducidas se limita a estos lambdas. Por ejemplo, el siguiente código no funcionará:

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

Referencias

Literales binarios

El prefijo 0b se puede usar para representar literales binarios.

Los literales binarios permiten construir números a partir de ceros y unos, lo que hace que sea mucho más fácil ver qué bits se establecen en la representación binaria de un número. Esto puede ser útil para trabajar con banderas binarias.

Las siguientes son formas equivalentes de especificar un int con valor 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

Enumeración de banderas

Antes, la especificación de valores de enum para una enum solo se podía hacer usando uno de los tres métodos en este ejemplo:

[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 los literales binarios es más obvio qué bits se configuran, y su uso no requiere entender los números hexadecimales y la aritmética a nivel de bits:

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

Separadores de dígitos

El subrayado _ se puede utilizar como un separador de dígitos. Ser capaz de agrupar dígitos en grandes literales numéricos tiene un impacto significativo en la legibilidad.

El subrayado puede aparecer en cualquier lugar en un literal numérico, excepto como se indica a continuación. Diferentes agrupamientos pueden tener sentido en diferentes escenarios o con diferentes bases numéricas.

Cualquier secuencia de dígitos puede estar separada por uno o más guiones bajos. Se permite el _ en decimales así como exponentes. Los separadores no tienen impacto semántico, simplemente se ignoran.

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;

Donde el separador de dígitos _ no puede ser utilizado:

  • al comienzo del valor ( _121 )
  • al final del valor ( 121_ o 121.05_ )
  • al lado del decimal ( 10_.0 )
  • junto al personaje exponente ( 1.1e_1 )
  • junto al especificador de tipo ( 10_f )
  • inmediatamente después de 0x o 0b en literales binarios y hexadecimales ( puede cambiarse para permitir, por ejemplo, 0b_1001_1000 )

Soporte de idioma para Tuplas

Lo esencial

Una tupla es una lista ordenada y finita de elementos. Las tuplas se usan comúnmente en la programación como un medio para trabajar con una sola entidad colectivamente en lugar de trabajar individualmente con cada uno de los elementos de la tupla, y para representar filas individuales (es decir, "registros") en una base de datos relacional.

En C # 7.0, los métodos pueden tener múltiples valores de retorno. Detrás de escena, el compilador utilizará la nueva estructura ValueTuple .

public (int sum, int count) GetTallies() 
{
    return (1, 2);
}

Nota al System.ValueTuple : para que esto funcione en Visual Studio 2017, necesita obtener el paquete System.ValueTuple .

Si se asigna un resultado del método de devolución de la tupla a una sola variable, puede acceder a los miembros por sus nombres definidos en la firma del método:

var result = GetTallies();
// > result.sum
// 1
// > result.count
// 2

Deconstrucción de tuplas

La deconstrucción de la tupla separa una tupla en sus partes.

Por ejemplo, invocar GetTallies y asignar el valor de retorno a dos variables separadas deconstruye la tupla en esas dos variables:

(int tallyOne, int tallyTwo) = GetTallies();

var también funciona:

(var s, var c) = GetTallies();

También puede usar una sintaxis más corta, con var fuera de () :

var (s, c) = GetTallies();

También puedes deconstruir en variables existentes:

int s, c;
(s, c) = GetTallies();

El intercambio es ahora mucho más simple (no se necesita una variable temporal):

(b, a) = (a, b);

Curiosamente, cualquier objeto se puede deconstruir definiendo un método Deconstruct en la clase:

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;

En este caso, la (localFirstName, localLastName) = person invoca a Deconstruct en la person .

La deconstrucción puede incluso definirse en un método de extensión. Esto es equivalente a lo anterior:

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 enfoque alternativo para la clase Person es definir el Name sí mismo como un Tuple . Considera lo siguiente:

class Person
{
    public (string First, string Last) Name { get; }

    public Person((string FirstName, string LastName) name)
    {
        Name = name;
    }
}

Luego, puede crear una instancia de una persona así (donde podemos tomar una tupla como argumento):

var person = new Person(("Jane", "Smith"));

var firstName = person.Name.First; // "Jane"
var lastName = person.Name.Last;   // "Smith"

Inicialización de la tupla

También puede crear arbitrariamente tuplas en el código:

var name = ("John", "Smith");
Console.WriteLine(name.Item1);
// Outputs John

Console.WriteLine(name.Item2);
// Outputs Smith

Al crear una tupla, puede asignar nombres de elementos ad-hoc a los miembros de la tupla:

var name = (first: "John", middle: "Q", last: "Smith");
Console.WriteLine(name.first);
// Outputs John

Inferencia de tipos

Las múltiples tuplas definidas con la misma firma (tipos coincidentes y recuento) se deducirán como tipos coincidentes. Por ejemplo:

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 se pueden devolver ya que la declaración de la variable de stats y la firma de devolución del método son una coincidencia.

Reflexión y nombres de campos de tuplas

Los nombres de los miembros no existen en tiempo de ejecución. Reflexión considerará que las tuplas con el mismo número y tipos de miembros son iguales, incluso si los nombres de los miembros no coinciden. Convertir una tupla en un object y luego en una tupla con los mismos tipos de miembros, pero con nombres diferentes, tampoco causará una excepción.

Si bien la clase ValueTuple en sí misma no conserva la información de los nombres de los miembros, la información está disponible a través de la reflexión en un TupleElementNamesAttribute. Este atributo no se aplica a la tupla en sí, sino a los parámetros del método, valores de retorno, propiedades y campos. Esto permite que los nombres de los elementos de la tupla se conserven en todos los ensamblajes, es decir, si un método devuelve (nombre de cadena, cuenta int), el nombre y el recuento estarán disponibles para los llamadores del método en otro ensamblaje porque el valor de retorno se marcará con TupleElementNameAttribute que contiene los valores "nombre" y "contar".

Utilizar con genéricos y async

Las nuevas características de la tupla (que utilizan el tipo ValueTuple subyacente) son totalmente compatibles con los genéricos y se pueden utilizar como parámetro de tipo genérico. Eso hace que sea posible usarlos con el patrón async / await :

public async Task<(string value, int count)> GetValueAsync()
{
    string fooBar = await _stackoverflow.GetStringAsync();
    int num = await _stackoverflow.GetIntAsync();

    return (fooBar, num);
}

Usar con colecciones

Puede ser beneficioso tener una colección de tuplas en (como ejemplo) un escenario en el que intenta encontrar una tupla que coincida con las condiciones para evitar la bifurcación de códigos.

Ejemplo:

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 las nuevas tuplas pueden convertirse:

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

Aunque la denominación en la tupla de ejemplo anterior es bastante genérica, la idea de etiquetas relevantes permite una comprensión más profunda de lo que se intenta en el código que hace referencia a "item1", "item2", y "item3".

Diferencias entre ValueTuple y Tuple

La razón principal para la introducción de ValueTuple es el rendimiento.

Escribe un nombre ValueTuple Tuple
Clase o estructura struct class
Mutabilidad (cambio de valores después de la creación) mudable inmutable
Nombrando miembros y otro soporte de idioma no ( TBD )

Referencias

Funciones locales

Las funciones locales se definen dentro de un método y no están disponibles fuera de él. Tienen acceso a todas las variables locales y admiten iteradores, async / await y lambda sintaxis. De esta manera, las repeticiones específicas de una función se pueden funcionalizar sin sobrecargar a la clase. Como efecto secundario, esto mejora el rendimiento de la sugerencia inteligente.

Ejemplo

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

Las funciones locales simplifican considerablemente el código para los operadores LINQ, donde normalmente tiene que separar las verificaciones de argumentos de la lógica real para hacer que las verificaciones de los argumentos sean instantáneas, no se demore hasta después de que se inicie la iteración.

Ejemplo

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

Las funciones locales también soportan el async y await palabras clave.

Ejemplo

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 que puede haber notado es que las funciones locales se pueden definir bajo la declaración de return , no es necesario que estén definidas arriba. Además, las funciones locales suelen seguir la convención de denominación "lowerCamelCase" para diferenciarse más fácilmente de las funciones de alcance de clase.

La coincidencia de patrones

Las extensiones de coincidencia de patrones para C # permiten muchos de los beneficios de la coincidencia de patrones de lenguajes funcionales, pero de una manera que se integra sin problemas con la sensación del lenguaje subyacente

switch expresión

La coincidencia de patrones se extiende el switch declaración para encender tipos:

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 expresión

La coincidencia de patrones extiende el operador is para verificar un tipo y declarar una nueva variable al mismo tiempo.

Ejemplo

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

Se puede reescribir como

7.0
if(o is string s)
{
    //Do something with s
};

También tenga en cuenta que el alcance de la variable de patrón s se extiende hacia fuera del bloque if que llega al final del alcance que lo rodea, por ejemplo:

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 retorno y ref local

Los retornos de referencia y los locales de referencia son útiles para manipular y devolver referencias a bloques de memoria en lugar de copiar memoria sin recurrir a punteros no seguros.

Retorno de referencia

public static ref TValue Choose<TValue>(
    Func<bool> condition, ref TValue left, ref TValue right)
{
    return condition() ? ref left : ref right;
}

Con esto, puede pasar dos valores por referencia y uno de ellos se devuelve en función de alguna condición:

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

Operaciones de referencia inseguras

En System.Runtime.CompilerServices.Unsafe se ha definido un conjunto de operaciones no seguras que le permiten manipular los valores de ref como si fueran punteros, básicamente.

Por ejemplo, reinterpretando una dirección de memoria ( ref ) como un tipo diferente:

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

Sin embargo, BitConverter.IsLittleEndian cuidado con el endianness al hacer esto, por ejemplo, verifique BitConverter.IsLittleEndian si es necesario y maneje en consecuencia.

O iterar sobre una matriz de una manera insegura:

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

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

Además, uno puede verificar si dos valores de ref son iguales, es decir, la misma dirección:

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

Campo de golf

Tema de Roslyn Github

System.Runtime.CompilerServices.Unsafe en github

lanzar expresiones

C # 7.0 permite lanzar como una expresión en ciertos lugares:

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

Antes de C # 7.0, si deseaba lanzar una excepción desde un cuerpo de expresión, tendría que:

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");
      })();

En C # 7.0, lo anterior ahora está simplificado para:

var spoonsArray = spoons.Length > 0 ? spoons : throw new Exception("There are no spoons");

Expresión extendida lista de miembros con cuerpo

C # 7.0 agrega accesores, constructores y finalizadores a la lista de cosas que pueden tener cuerpos de expresión:

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

También vea la sección de declaración de var para el operador de descarte.

ValueTask

Task<T> es una clase y provoca una sobrecarga innecesaria de su asignación cuando el resultado está disponible de inmediato.

ValueTask<T> es una estructura y se ha introducido para evitar la asignación de un objeto Task en caso de que el resultado de la operación asíncrona ya esté disponible en el momento de la espera.

Entonces, ValueTask<T> proporciona dos beneficios:

1. Aumento de rendimiento

Aquí hay un ejemplo de Task<T> :

  • Requiere asignación de montón
  • Toma 120ns con JIT
async Task<int> TestTask(int d)
{
    await Task.Delay(d);
    return 10;
}

Aquí está el ejemplo de ValueTask<T> analógico:

  • Sin asignación del montón si el resultado se conoce de forma sincrónica (que no lo es en este caso debido a la Task.Delay , pero a menudo se encuentra en muchos en el mundo real async / await escenarios)
  • Toma 65ns con JIT
async ValueTask<int> TestValueTask(int d)
{
    await Task.Delay(d);
    return 10;
}

2. Mayor flexibilidad de implementación

De lo contrario, las implementaciones de una interfaz asíncrona que deseen ser síncronas se verían obligadas a utilizar Task.Run o Task.FromResult (que resulta en la penalización de rendimiento analizada anteriormente). Por lo tanto hay una cierta presión contra implementaciones sincrónicas.

Pero con ValueTask<T> , las implementaciones son más libres de elegir entre ser síncronas o asíncronas sin afectar a las personas que llaman.

Por ejemplo, aquí hay una interfaz con un método asíncrono:

interface IFoo<T>
{
    ValueTask<T> BarAsync();
}

... y así es como se podría llamar ese método:

IFoo<T> thing = getThing();
var x = await thing.BarAsync();

Con ValueTask , el código anterior funcionará con implementaciones síncronas o asíncronas :

Implementación síncrona:

class SynchronousFoo<T> : IFoo<T>
{
    public ValueTask<T> BarAsync()
    {
        var value = default(T);
        return new ValueTask<T>(value);
    }
}

Implementación asíncrona

class AsynchronousFoo<T> : IFoo<T>
{
    public async ValueTask<T> BarAsync()
    {
        var value = default(T);
        await Task.Delay(1);
        return value;
    }
}

Notas

Aunque se estaba planeando ValueTask estructura ValueTask a C # 7.0 , por el momento se ha mantenido como otra biblioteca. ValueTask <T> System.Threading.Tasks.Extensions paquete se puede descargar desde la Galería Nuget



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow