Novedades de C# 7.0What's new in C# 7.0

C# 7.0 incorpora varias características nuevas al lenguaje C#:C# 7.0 adds a number of new features to the C# language:

  • Variables de outout variables
    • Puede declarar valores out que se inserten como argumentos en el método en que se usen.You can declare out values inline as arguments to the method where they are used.
  • TuplasTuples
    • Puede crear tipos ligeros sin nombre que contengan varios campos públicos.You can create lightweight, unnamed types that contain multiple public fields. Los compiladores y las herramientas IDE comprenden la semántica de estos tipos.Compilers and IDE tools understand the semantics of these types.
  • DescartesDiscards
    • Los descartes son variables temporales y de solo escritura que se usan en argumentos cuando el valor asignado es indiferente.Discards are temporary, write-only variables used in assignments when you don't care about the value assigned. Son especialmente útiles al deconstruir tuplas y tipos definidos por el usuario, así como al realizar llamadas a métodos mediante parámetros de out.They are particularly useful when deconstructing tuples and user-defined types, as well as when calling methods with out parameters.
  • Coincidencia de patronesPattern Matching
    • Puede crear la lógica de bifurcación en función de tipos y valores arbitrarios de los miembros de esos tipos.You can create branching logic based on arbitrary types and values of the members of those types.
  • Devoluciones y variables locales refref locals and returns
    • Las variables locales de método y los valores devueltos pueden ser referencias a otro almacenamiento.Method local variables and return values can be references to other storage.
  • Funciones localesLocal Functions
    • Puede anidar funciones en otras funciones para limitar su ámbito y visibilidad.You can nest functions inside other functions to limit their scope and visibility.
  • Más miembros con forma de expresiónMore expression-bodied members
    • La lista de miembros que se pueden crear con expresiones ha crecido.The list of members that can be authored using expressions has grown.
  • Expresiones throwthrow Expressions
    • Puede iniciar excepciones en construcciones de código que antes no se permitían porque throw era una instrucción.You can throw exceptions in code constructs that previously were not allowed because throw was a statement.
  • Tipos de valor devueltos de async generalizadosGeneralized async return types
    • Los métodos declarados con el modificador async pueden devolver otros tipos además de Task y Task<T>.Methods declared with the async modifier can return other types in addition to Task and Task<T>.
  • Mejoras en la sintaxis de literales numéricosNumeric literal syntax improvements
    • Nuevos tokens mejoran la legibilidad de las constantes numéricas.New tokens improve readability for numeric constants.

En el resto de este tema se tratan cada una de las características.The remainder of this topic discusses each of the features. Para cada característica, conocerá el razonamiento subyacente.For each feature, you'll learn the reasoning behind it. Aprenderá la sintaxis.You'll learn the syntax. Verá algunos escenarios de ejemplo en que, al usar la nueva característica, será más productivo como desarrollador.You'll see some sample scenarios where using the new feature will make you more productive as a developer.

Variables outout variables

En esta versión se ha mejorado la sintaxis existente que admite parámetros out.The existing syntax that supports out parameters has been improved in this version.

Anteriormente, tendría que separar la declaración de la variable out y su inicialización en dos instrucciones diferentes:Previously, you would need to separate the declaration of the out variable and its initialization into two different statements:

int numericResult;
if (int.TryParse(input, out numericResult))
    Console.WriteLine(numericResult);
else
    Console.WriteLine("Could not parse input");

Ahora puede declarar variables out en la lista de argumentos de una llamada a método, en lugar de escribir una instrucción de declaración distinta:You can now declare out variables in the argument list of a method call, rather than writing a separate declaration statement:

if (int.TryParse(input, out int result))
    Console.WriteLine(result);
else
    Console.WriteLine("Could not parse input");

Para mayor claridad, puede que prefiera especificar el tipo de la variable out, tal y como se muestra anteriormente.You may want to specify the type of the out variable for clarity, as shown above. Pero el lenguaje admite el uso de una variable local con tipo implícito:However, the language does support using an implicitly typed local variable:

if (int.TryParse(input, out var answer))
    Console.WriteLine(answer);
else
    Console.WriteLine("Could not parse input");
  • El código es más fácil de leer.The code is easier to read.
    • Declare la variable out donde la use, no en otra línea anterior.You declare the out variable where you use it, not on another line above.
  • No es preciso asignar ningún valor inicial.No need to assign an initial value.
    • Al declarar la variable out donde se usa en una llamada a método, no podrá usarla accidentalmente antes de que se asigne.By declaring the out variable where it is used in a method call, you can't accidentally use it before it is assigned.

El uso más común de esta característica será el patrón Try.The most common use for this feature will be the Try pattern. En este patrón, un método devuelve un bool que indica éxito o error y una variable out que proporciona el resultado si el método tiene éxito.In this pattern, a method returns a bool indicating success or failure and an out variable that provides the result if the method succeeds.

Cuando se usa la declaración de variable out, la variable declarada "se pierde" en el ámbito externo de la instrucción if.When using the out variable declaration, the declared variable "leaks" into the outer scope of the if statement. Esto permite usar la variable después:This allows you to use the variable afterwards:

if (!int.TryParse(input, out int result))
{    
    return null;
}

return result;

TuplasTuples

Nota

Las nuevas características de tupla requieren los tipos ValueTuple.The new tuples features require the ValueTuple types. Debe agregar el paquete NuGet System.ValueTuple para usarlo en plataformas que no incluyen los tipos.You must add the NuGet package System.ValueTuple in order to use it on platforms that do not include the types.

Esto es similar a otras características del lenguaje que se basan en tipos que se han proporcionado en el marco.This is similar to other language features that rely on types delivered in the framework. El ejemplo incluye async y await, que se basan en la interfaz INotifyCompletion. LINQ se basa en IEnumerable<T>.Example include async and await relying on the INotifyCompletion interface, and LINQ relying on IEnumerable<T>. En cambio, el mecanismo de entrega está cambiando a medida que .NET está pasando a ser más independiente de las plataformas.However, the delivery mechanism is changing as .NET is becoming more platform independent. Puede que .NET Framework no proporcione siempre la misma cadencia que el compilador de lenguaje.The .NET Framework may not always ship on the same cadence as the language compiler. Cuando las nuevas características del lenguaje se basan en tipos nuevos, esos tipos estarán disponibles como paquetes NuGet cuando se proporcionen las características del lenguaje.When new language features rely on new types, those types will be available as NuGet packages when the language features ship. Como estos tipos nuevos se agregan a la API de .NET Standard y se proporcionan como parte del marco, el requisito del paquete NuGet se quitará.As these new types get added to the .NET Standard API and delivered as part of the framework, the NuGet package requirement will be removed.

C# ofrece una sintaxis enriquecida para clases y estructuras que se usa para explicar la intención del diseño.C# provides a rich syntax for classes and structs that is used to explain your design intent. Pero a veces esa sintaxis enriquecida requiere trabajo adicional con apenas beneficio.But sometimes that rich syntax requires extra work with minimal benefit. Puede que a menudo escriba métodos que requieren una estructura simple que contenga más de un elemento de datos.You may often write methods that need a simple structure containing more than one data element. Para admitir estos escenarios, se han agregado tuplas a C#.To support these scenarios tuples were added to C#. Las tuplas son estructuras de datos ligeros que contienen varios campos para representar los miembros de datos.Tuples are lightweight data structures that contain multiple fields to represent the data members. Los campos no se validan y no se pueden definir métodos propiosThe fields are not validated, and you cannot define your own methods

Nota

Las tuplas estaban disponibles antes de C# 7.0, pero no eran eficientes ni compatibles con ningún lenguaje.Tuples were available before C# 7.0, but they were inefficient and had no language support. Esto significaba que solo se podía hacer referencia a los elementos tupla como Item1, Item2, por ejemplo.This meant that tuple elements could only be referenced as Item1, Item2 and so on. C# 7.0 presenta la compatibilidad de lenguaje con las tuplas, que permite usar nombres semánticos en los campos de una tupla mediante tipos de tupla nuevos y más eficientes.C# 7.0 introduces language support for tuples, which enables semantic names for the fields of a tuple using new, more efficient tuple types.

Puede crear una tupla asignando un valor a cada miembro:You can create a tuple by assigning a value to each member:

var letters = ("a", "b");

Esta asignación crea una tupla con los miembros Item1 y Item2. Se puede usar del mismo modo que Tuple. Además, puede cambiar la sintaxis para crear una tupla que proporciona nombres de semántica a cada uno de sus miembros:That assignment creates a tuple whose members are Item1 and Item2, which you can use in the same way as Tuple You can change the syntax to create a tuple that provides semantic names to each of the members of the tuple:

(string Alpha, string Beta) namedLetters = ("a", "b");

La tupla namedLetters contiene campos denominados Alpha y Beta.The namedLetters tuple contains fields referred to as Alpha and Beta. Esos nombres solo existen en el tiempo de compilación y no se conservan, por ejemplo, al inspeccionar la tupla mediante una reflexión en tiempo de ejecución.Those names exist only at compile time and are not preserved for example when inspecting the tuple using reflection at runtime.

En la asignación de una tupla, también pueden especificarse los nombres de los campos a la derecha de la asignación:In a tuple assignment, you can also specify the names of the fields on the right-hand side of the assignment:

var alphabetStart = (Alpha: "a", Beta: "b");

Puede especificar nombres para los campos a la izquierda y la derecha de la asignación:You can specify names for the fields on both the left and right-hand side of the assignment:

(string First, string Second) firstLetters = (Alpha: "a", Beta: "b");

La línea anterior genera una advertencia, CS8123, que indica que los nombres a la derecha de la asignación, Alpha y Beta, se omiten porque entran en conflicto con los nombres a la izquierda, First y Second.The line above generates a warning, CS8123, telling you that the names on the right side of the assignment, Alpha and Beta are ignored because they conflict with the names on the left side, First and Second.

Los ejemplos anteriores muestran la sintaxis básica para declarar tuplas.The examples above show the basic syntax to declare tuples. Las tuplas resultan más útiles como tipos de valor devuelto para los métodos private y internal.Tuples are most useful as return types for private and internal methods. Las tuplas proporcionan una sintaxis sencilla para que esos métodos devuelvan varios valores discretos: Se ahorra el trabajo de crear un class o un struct que defina el tipo devuelto.Tuples provide a simple syntax for those methods to return multiple discrete values: You save the work of authoring a class or a struct that defines the type returned. No hay necesidad de crear otra tupla.There is no need for creating a new type.

La creación de tuplas resulta más eficiente y productiva.Creating a tuple is more efficient and more productive. Se trata de una sintaxis ligera y más sencilla para definir una estructura de datos que incluya más de un valor.It is a simpler, lightweight syntax to define a data structure that carries more than one value. El siguiente método de ejemplo devuelve los valores mínimos y máximos de una secuencia de enteros:The example method below returns the minimum and maximum values found in a sequence of integers:

private static (int Max, int Min) Range(IEnumerable<int> numbers)
{
    int min = int.MaxValue;
    int max = int.MinValue;
    foreach(var n in numbers)
    {
        min = (n < min) ? n : min;
        max = (n > max) ? n : max;
    }
    return (max, min);
}

El uso de tuplas de esta manera supone varias ventajas:Using tuples in this way offers several advantages:

  • Se ahorra el trabajo de crear un class o un struct que defina el tipo devuelto.You save the work of authoring a class or a struct that defines the type returned.
  • No tiene que crear otro tipo.You do not need to create new type.
  • Las mejoras del lenguaje hacen innecesario llamar al método Create<T1>(T1).The language enhancements removes the need to call the Create<T1>(T1) methods.

La declaración del método proporciona los nombres de los campos de la tupla que se devuelve.The declaration for the method provides the names for the fields of the tuple that is returned. Cuando se llama al método, el valor devuelto es una tupla cuyos campos son Max y Min:When you call the method, the return value is a tuple whose fields are Max and Min:

var range = Range(numbers);

Puede que a veces quiera desempaquetar los miembros de una tupla devueltos de un método.There may be times when you want to unpackage the members of a tuple that were returned from a method. Para ello, declare distintas variables para cada uno de los valores de la tupla.You can do that by declaring separate variables for each of the values in the tuple. Esto se denomina deconstruir la tupla:This is called deconstructing the tuple:

(int max, int min) = Range(numbers);

También puede proporcionar una deconstrucción similar para cualquier tipo de .NET.You can also provide a similar deconstruction for any type in .NET. Para ello, escriba un método Deconstruct como miembro de la clase.This is done by writing a Deconstruct method as a member of the class. Ese método Deconstruct proporciona un conjunto de argumentos out para cada una de las propiedades que quiere extraer.That Deconstruct method provides a set of out arguments for each of the properties you want to extract. Tenga en cuenta que esta clase Point proporciona un método deconstructor que extrae las coordenadas X e Y:Consider this Point class that provides a deconstructor method that extracts the X and Y coordinates:

public class Point
{
    public Point(double x, double y)
    {
        this.X = x;
        this.Y = y;
    }

    public double X { get; }
    public double Y { get; }

    public void Deconstruct(out double x, out double y)
    {
        x = this.X;
        y = this.Y;
    }
}

Puede extraer los campos individuales asignando un Point a una tupla:You can extract the individual fields by assigning a Point to a tuple:

var p = new Point(3.14, 2.71);
(double X, double Y) = p;

No está obligado por los nombres definidos en el método Deconstruct.You are not bound by the names defined in the Deconstruct method. Puede cambiar el nombre de las variables de extracción como parte de la asignación:You can rename the extract variables as part of the assignment:

(double horizontalDistance, double verticalDistance) = p;

Puede aprender más sobre tuplas en el tema sobre tuplas.You can learn more in depth about tuples in the tuples topic.

DescartesDiscards

Habitualmente, al deconstruir una tupla o realizar una llamada a un método mediante parámetros out, debe definir una variable con un valor indiferente y que no vaya a usar.Often when deconstructing a tuple or calling a method with out parameters, you're forced to define a variable whose value you don't care about and don't intend to use. C# agrega la compatibilidad con descartes para gestionar este tipo de escenarios.C# adds support for discards to handle this scenario. Un descarte es una variable de solo escritura con el nombre _ (el carácter de guion bajo). Puede asignar todos los valores que quiera descartar a una única variable.A discard is a write-only variable whose name is _ (the underscore character); you can assign all of the values that you intend to discard to the single variable. Un descarte se parece a una variable no asignada, aunque no puede usarse en el código (excepto la instrucción de asignación).A discard is like an unassigned variable; apart from the assignment statement, the discard can't be used in code.

Los descartes se admiten en los escenarios siguientes:Discards are supported in the following scenarios:

  • Al deconstruir tuplas o tipos definidos por el usuario.When deconstructing tuples or user-defined types.

  • Al realizar llamadas a métodos mediante parámetros out.When calling methods with out parameters.

  • En una operación de coincidencia de patrones con las instrucciones is y switch.In a pattern matching operation with the is and switch statements.

  • Como un identificador independiente cuando quiera identificar explícitamente el valor de una asignación como descarte.As a standalone identifier when you want to explicitly identify the value of an assignment as a discard.

En el ejemplo siguiente se define un método QueryCityDataForYears que devuelve una tupla de tipo 6 con datos de una ciudad correspondientes a dos años diferentes.The following example defines a QueryCityDataForYears method that returns a 6-tuple that contains a data for a city for two different years. La llamada de método del ejemplo se refiere únicamente a los dos valores de rellenado que devuelve el método, por lo que trata los valores restantes de la tupla como descartes al deconstruirla.The method call in the example is concerned only with the two population values returned by the method and so treats the remaining values in the tuple as discards when it deconstructs the tuple.

using System;
using System.Collections.Generic;

public class Example
{
   public static void Main()
   {
       var (_, _, _, pop1, _, pop2) = QueryCityDataForYears("New York City", 1960, 2010);

       Console.WriteLine($"Population change, 1960 to 2010: {pop2 - pop1:N0}");
   }
   
   private static (string, double, int, int, int, int) QueryCityDataForYears(string name, int year1, int year2)
   {
      int population1 = 0, population2 = 0;
      double area = 0;
      
      if (name == "New York City") {
         area = 468.48; 
         if (year1 == 1960) {
            population1 = 7781984;
         }
         if (year2 == 2010) {
            population2 = 8175133;
         }
      return (name, area, year1, population1, year2, population2);
      }

      return ("", 0, 0, 0, 0, 0);
   }
}
// The example displays the following output:
//      Population change, 1960 to 2010: 393,149

Para obtener más información, vea Descartes.For more information, see Discards.

Detección de patronesPattern matching

La coincidencia de patrones es una característica que permite implementar la distribución de métodos en propiedades distintas al tipo de un objeto.Pattern matching is a feature that allows you to implement method dispatch on properties other than the type of an object. Probablemente ya esté familiarizado con la distribución de métodos en función del tipo de un objeto.You're probably already familiar with method dispatch based on the type of an object. En la programación orientada a objetos, los métodos virtuales y de invalidación proporcionan la sintaxis del lenguaje para implementar la distribución de métodos en función del tipo de un objeto.In Object Oriented programming, virtual and override methods provide language syntax to implement method dispatching based on an object's type. Las clases base y derivadas proporcionan distintas implementaciones.Base and Derived classes provide different implementations. Las expresiones de coincidencia de patrones extienden este concepto para que se puedan implementar fácilmente patrones de distribución similares para tipos y elementos de datos que no se relacionan mediante una jerarquía de herencia.Pattern matching expressions extend this concept so that you can easily implement similar dispatch patterns for types and data elements that are not related through an inheritance hierarchy.

La coincidencia de patrones admite expresiones is y switch.Pattern matching supports is expressions and switch expressions. Cada una de ellas habilita la inspección de un objeto y sus propiedades para determinar si el objeto cumple el patrón buscado.Each enables inspecting an object and its properties to determine if that object satisfies the sought pattern. Use la palabra clave when para especificar reglas adicionales para el patrón.You use the when keyword to specify additional rules to the pattern.

Expresión isis expression

La expresión de patrón is extiende el conocido operador is para consultar un objeto más allá de su tipo.The is pattern expression extends the familiar is operator to query an object beyond its type.

Comencemos por un escenario sencillo.Let's start with a simple scenario. Agregaremos funciones a este escenario que muestren cómo las expresiones de coincidencia de patrones crean algoritmos que funcionan fácilmente con tipos no relacionados.We'll add capabilities to this scenario that demonstrate how pattern matching expressions make algorithms that work with unrelated types easy. Empezaremos por un método que calcule la suma de varias tiradas de dados:We'll start with a method that computes the sum of a number of die rolls:

public static int DiceSum(IEnumerable<int> values)
{
    return values.Sum();
}

Es posible que descubra rápidamente que tiene que encontrar la suma de tiradas de dados en las que algunas de las tiradas se realizan con más de un dado.You might quickly find that you need to find the sum of die rolls where some of the rolls are made with multiple dice (dice is the plural of die). Puede que parte de la secuencia de entrada tenga varios resultados en lugar de un solo número:Part of the input sequence may be multiple results instead of a single number:

public static int DiceSum2(IEnumerable<object> values)
{
    var sum = 0;
    foreach(var item in values)
    {
        if (item is int val)
            sum += val;
        else if (item is IEnumerable<object> subList)
            sum += DiceSum2(subList);
    }
    return sum;
}

La expresión de patrón is funciona bastante bien en este escenario.The is pattern expression works quite well in this scenario. Como parte de la comprobación del tipo, escriba una variable de inicialización.As part of checking the type, you write a variable initialization. Esta crea otra variable del tipo de tiempo de ejecución validado.This creates a new variable of the validated runtime type.

Mientras siga extendiendo estos escenarios, puede que vea que crea más instrucciones if y else if.As you keep extending these scenarios, you may find that you build more if and else if statements. Cuando esto resulte poco manejable, probablemente prefiera cambiar a expresiones de patrón switch.Once that becomes unwieldy, you'll likely want to switch to switch pattern expressions.

Actualizaciones de instrucciones switchswitch statement updates

La expresión de coincidencia tiene una sintaxis conocida, que se basa en la instrucción switch que ya forma parte del lenguaje C#.The match expression has a familiar syntax, based on the switch statement already part of the C# language. Vamos a traducir el código existente para usar una expresión de coincidencia antes de agregar nuevos casos:Let's translate the existing code to use a match expression before adding new cases:

public static int DiceSum3(IEnumerable<object> values)
{
    var sum = 0;
    foreach (var item in values)
    {
        switch (item)
        {
            case int val:
                sum += val;
                break;
            case IEnumerable<object> subList:
                sum += DiceSum3(subList);
                break;
        }
    }
    return sum;
}

Las expresiones de coincidencia tienen una sintaxis ligeramente distinta que las expresiones is, donde el tipo y la variable se declaran al principio de la expresión case.The match expressions have a slightly different syntax than the is expressions, where you declare the type and variable at the beginning of the case expression.

Las expresiones de coincidencia también admiten constantes.The match expressions also support constants. Esto puede ahorrar tiempo al no tener en cuenta casos sencillos:This can save time by factoring out simple cases:

public static int DiceSum4(IEnumerable<object> values)
{
    var sum = 0;
    foreach (var item in values)
    {
        switch (item)
        {
            case 0:
                break;
            case int val:
                sum += val;
                break;
            case IEnumerable<object> subList when subList.Any():
                sum += DiceSum4(subList);
                break;
            case IEnumerable<object> subList:
                break;
            case null:
                break;
            default:
                throw new InvalidOperationException("unknown item type");
        }
    }
    return sum;
}

El código anterior agrega casos de 0 como un caso especial de int y null como un caso especial cuando no hay ninguna entrada.The code above adds cases for 0 as a special case of int, and null as a special case when there is no input. Muestra una nueva característica importante en expresiones de patrón de modificador: el orden de las expresiones case ahora cuenta.This demonstrates one important new feature in switch pattern expressions: the order of the case expressions now matters. El caso 0 debe aparecer antes del caso general int.The 0 case must appear before the general int case. De lo contrario, el primer patrón que coincida sería el caso int, aunque el valor sea 0.Otherwise, the first pattern to match would be the int case, even when the value is 0. Si accidentalmente ordena expresiones de coincidencia como estas una vez controlado el último caso, el compilador las marcará y generará un error.If you accidentally order match expressions such that a later case has already been handled, the compiler will flag that and generate an error.

Este mismo comportamiento habilita el caso especial en una secuencia de entrada vacía.This same behavior enables the special case for an empty input sequence. Puede ver que el caso de un elemento IEnumerable que tiene elementos debe aparecer antes que el caso general IEnumerable.You can see that the case for an IEnumerable item that has elements must appear before the general IEnumerable case.

En esta versión se ha agregado también un caso default.This version has also added a default case. El caso default siempre se evalúa en último lugar, independientemente del orden en que aparezca en el origen.The default case is always evaluated last, regardless of the order it appears in the source. Por ese motivo, la convención es poner el caso default al final.For that reason, convention is to put the default case last.

Por último, agregaremos un último case para un nuevo estilo de dado.Finally, let's add one last case for a new style of die. Algunos juegos usan dados de percentil para representar intervalos mayores de números.Some games use percentile dice to represent larger ranges of numbers.

Nota

Dos dados de percentil de 10 caras pueden representar todos los números del 0 al 99.Two 10-sided percentile dice can represent every number from 0 through 99. Un dado tiene caras etiquetadas con 00, 10, 20, ...90.One die has sides labelled 00, 10, 20, ... 90. El otro tiene caras etiquetadas con 0, 1, 2, ...9.The other die has sides labeled 0, 1, 2, ... 9. Sume los valores de los dos dados y podrá obtener todos los números del 0 al 99.Add the two die values together and you can get every number from 0 through 99.

Para agregar este tipo de dado a la colección, defina primero un tipo que represente el dado de percentil.To add this kind of die to your collection, first define a type to represent the percentile dice. La propiedad TensDigit almacena hasta 90 valores 0, 10 y 20:The TensDigit property stores values 0, 10, 20, up to 90:

public struct PercentileDice
{
    public int OnesDigit { get; }
    public int TensDigit { get; }

    public PercentileDice(int tensDigit, int onesDigit)
    {
        this.OnesDigit = onesDigit;
        this.TensDigit = tensDigit;
    }
}

Después agregue una expresión de coincidencia case para el nuevo tipo:Then, add a case match expression for the new type:

public static int DiceSum5(IEnumerable<object> values)
{
    var sum = 0;
    foreach (var item in values)
    {
        switch (item)
        {
            case 0:
                break;
            case int val:
                sum += val;
                break;
            case PercentileDice dice:
                sum += dice.TensDigit + dice.OnesDigit;
                break;
            case IEnumerable<object> subList when subList.Any():
                sum += DiceSum5(subList);
                break;
            case IEnumerable<object> subList:
                break;
            case null:
                break;
            default:
                throw new InvalidOperationException("unknown item type");
        }
    }
    return sum;
}

La nueva sintaxis de expresiones de coincidencia de patrones facilita la creación de algoritmos de distribución basados en un tipo del objeto o en otras propiedades, mediante una sintaxis clara y concisa.The new syntax for pattern matching expressions makes it easier to create dispatch algorithms based on an object's type, or other properties, using a clear and concise syntax. Las expresiones de coincidencia de patrones habilitan estas construcciones en tipos de datos que no se relacionan por herencia.Pattern matching expressions enable these constructs on data types that are unrelated by inheritance.

Puede obtener más información sobre la coincidencia de patrones en el tema dedicado a coincidencia de patrones en C#.You can learn more about pattern matching in the topic dedicated to pattern matching in C#.

Devoluciones y variables locales refRef locals and returns

Esta característica habilita algoritmos que usan y devuelven referencias a variables definidas en otro lugar.This feature enables algorithms that use and return references to variables defined elsewhere. Por ejemplo, trabajar con matrices de gran tamaño y buscar una sola ubicación con determinadas características.One example is working with large matrices, and finding a single location with certain characteristics. Un método devolvería los dos índices para una sola ubicación en la matriz:One method would return the two indices for a single location in the matrix:

public static (int i, int j) Find(int[,] matrix, Func<int, bool> predicate)
{
    for (int i = 0; i < matrix.GetLength(0); i++)
        for (int j = 0; j < matrix.GetLength(1); j++)
            if (predicate(matrix[i, j]))
                return (i, j);
    return (-1, -1); // Not found
}

Existen varios problemas con este código.There are many issues with this code. En primer lugar, se trata de un método público que devuelve una tupla.First of all, it's a public method that's returning a tuple. El lenguaje admite esto, pero se recomienda usar tipos definidos por el usuario (clases o structs) para las API públicas.The language supports this, but user defined types (either classes or structs) are preferred for public APIs.

En segundo lugar, este método devuelve los índices al elemento de la matriz.Second, this method is returning the indices to the item in the matrix. Esto lleva a los autores de llamadas a escribir código que use esos índices para desreferenciar la matriz y modificar un solo elemento:That leads callers to write code that uses those indices to dereference the matrix and modify a single element:

var indices = MatrixSearch.Find(matrix, (val) => val == 42);
Console.WriteLine(indices);
matrix[indices.i, indices.j] = 24;

Es preferible escribir un método que devuelva una referencia al elemento de la matriz que quiere cambiar.You'd rather write a method that returns a reference to the element of the matrix that you want to change. Esto solo podría llevarse a cabo si usa un código no seguro y devuelve un puntero a un int en versiones anteriores.You could only accomplish this by using unsafe code and returning a pointer to an int in previous versions.

Repasaremos una serie de cambios que muestran la característica local ref y cómo crear un método que devuelva una referencia al almacenamiento interno.Let's walk through a series of changes to demonstrate the ref local feature and show how to create a method that returns a reference to internal storage. Por el camino, conocerá las reglas de devolución de ref y la característica de la variable local ref que impide que accidentalmente se haga un uso incorrecto de ella.Along the way, you'll learn the rules of the ref return and ref local feature that protects you from accidentally misusing it.

Empiece por modificar la declaración de método Find para que devuelva un ref int en lugar de una tupla.Start by modifying the Find method declaration so that it returns a ref int instead of a tuple. Luego modifique la instrucción return para que devuelva el valor almacenado en la matriz y no en los dos índices:Then, modify the return statement so it returns the value stored in the matrix instead of the two indices:

// Note that this won't compile. 
// Method declaration indicates ref return,
// but return statement specifies a value return.
public static ref int Find2(int[,] matrix, Func<int, bool> predicate)
{
    for (int i = 0; i < matrix.GetLength(0); i++)
        for (int j = 0; j < matrix.GetLength(1); j++)
            if (predicate(matrix[i, j]))
                return matrix[i, j];
    throw new InvalidOperationException("Not found");
}

Cuando se declara que un método devuelva una variable ref, debe agregarse también la palabra clave ref a cada instrucción return.When you declare that a method returns a ref variable, you must also add the ref keyword to each return statement. Esto indica una devolución por referencia y ayuda a que los desarrolladores que lean el código después recuerden que el método devuelve por referencia:That indicates return by reference, and helps developers reading the code later remember that the method returns by reference:

public static ref int Find3(int[,] matrix, Func<int, bool> predicate)
{
    for (int i = 0; i < matrix.GetLength(0); i++)
        for (int j = 0; j < matrix.GetLength(1); j++)
            if (predicate(matrix[i, j]))
                return ref matrix[i, j];
    throw new InvalidOperationException("Not found");
}

Ahora que el método devuelve una referencia al valor entero de la matriz, hay que modificar dónde se llama.Now that the method returns a reference to the integer value in the matrix, you need to modify where it's called. La declaración var indica que valItem es ahora un int en lugar de una tupla:The var declaration means that valItem is now an int rather than a tuple:

var valItem = MatrixSearch.Find3(matrix, (val) => val == 42);
Console.WriteLine(valItem);
valItem = 24;
Console.WriteLine(matrix[4, 2]);

La segunda instrucción WriteLine del ejemplo anterior imprime el valor 42, no 24.The second WriteLine statement in the example above prints out the value 42, not 24. La variable valItem es un int, no un ref int.The variable valItem is an int, not a ref int. La palabra clave var permite que el compilador especifique el tipo, pero no agregará implícitamente el modificador ref.The var keyword enables the compiler to specify the type, but will not implicitly add the ref modifier. En su lugar, el valor al que hace referencia el ref return se copia en la variable a la izquierda de la asignación.Instead, the value referred to by the ref return is copied to the variable on the left-hand side of the assignment. La variable no es una variable local ref.The variable is not a ref local.

Para obtener el resultado que quiere, debe agregar el modificador ref a la declaración de variable local para convertir la variable en una referencia cuando el valor devuelto sea una referencia:In order to get the result you want, you need to add the ref modifier to the local variable declaration to make the variable a reference when the return value is a reference:

ref var item = ref MatrixSearch.Find3(matrix, (val) => val == 42);
Console.WriteLine(item);
item = 24;
Console.WriteLine(matrix[4, 2]);

Ahora, la segunda instrucción WriteLine del ejemplo anterior imprimirá el valor 24, que indica que se ha modificado el almacenamiento en la matriz.Now, the second WriteLine statement in the example above will print out the value 24, indicating that the storage in the matrix has been modified. La variable local se ha declarado con el modificador ref y tomará un valor devuelto ref.The local variable has been declared with the ref modifier, and it will take a ref return. Debe inicializar una variable ref cuando se declara, no puede separar la declaración de la inicialización.You must initialize a ref variable when it is declared, you cannot split the declaration and the initialization.

El lenguaje C# tiene otras tres reglas que protegen del uso incorrecto de las variables locales y devoluciones de ref:The C# language has three other rules that protect you from misusing the ref locals and returns:

  • No se puede asignar un valor devuelto del método estándar para una variable local ref.You cannot assign a standard method return value to a ref local variable.
    • No permite instrucciones como ref int i = sequence.Count();That disallows statements like ref int i = sequence.Count();
  • No puede devolver un ref a una variable cuya duración se extiende más allá de la ejecución del método.You cannot return a ref to a variable whose lifetime does not extend beyond the execution of the method.
    • Esto significa que no puede devolver una referencia a una variable local o a una variable con un ámbito similar.That means you cannot return a reference to a local variable or a variable with a similar scope.
  • Las ref locales y las devoluciones no se pueden usar con métodos asíncronos.ref locals and returns can't be used with async methods.
    • El compilador no puede identificar si una variable a la que se hace referencia se ha establecido en su valor final en la devolución del método asíncrono.The compiler can't know if the referenced variable has been set to its final value when the async method returns.

La incorporación de variables locales ref y devoluciones de ref permite usar algoritmos que resultan más eficientes si se evita copiar los valores o se realizan operaciones de desreferencia varias veces.The addition of ref locals and ref returns enable algorithms that are more efficient by avoiding copying values, or performing dereferencing operations multiple times.

Agregar ref al valor devuelto es un cambio compatible con el origen.Adding ref to the return value is a source compatible change. El código existente se compila, pero el valor devuelto de referencia se copia cuando se asigna.Existing code compiles, but the ref return value is copied when assigned. Los autores de las llamadas deben actualizar el almacenamiento para el valor devuelto en una variable local ref para almacenar el valor devuelto como referencia.Callers must update the storage for the return value to a ref local variable to store the return as a reference.

Para más información, consulte el artículo sobre la palabra clave ref.For more information, see the ref keyword article.

Funciones localesLocal functions

Muchos diseños de clases incluyen métodos que se llaman desde una sola ubicación.Many designs for classes include methods that are called from only one location. Estos métodos privados adicionales mantienen cada método pequeño y centrado.These additional private methods keep each method small and focused. Pero pueden complicar la comprensión de una clase cuando se lee por primera vez.However, they can make it harder to understand a class when reading it the first time. Estos métodos deben entenderse fuera del contexto de la ubicación de llamada única.These methods must be understood outside of the context of the single calling location.

En esos diseños, las funciones locales permiten declarar métodos en el contexto de otro método.For those designs, local functions enable you to declare methods inside the context of another method. Esto facilita que los lectores de la clase vean que el método local solo se llama desde el contexto en el que se declara.This makes it easier for readers of the class to see that the local method is only called from the context in which is it declared.

Hay dos casos de uso muy común para funciones locales: métodos de iterador públicos y métodos asincrónicos públicos.There are two very common use cases for local functions: public iterator methods and public async methods. Ambos tipos de métodos generan código que informa de errores más tarde de lo que los programadores podrían esperar.Both types of methods generate code that reports errors later than programmers might expect. En el caso de los métodos de iterador, las excepciones solo se observan al llamar a código que enumera la secuencia devuelta.In the case of iterator methods, any exceptions are observed only when calling code that enumerates the returned sequence. En el caso de los métodos asincrónicos, las excepciones solo se observan cuando se espera al Task devuelto.In the case of async methods, any exceptions are only observed when the returned Task is awaited.

Empecemos por un método de iterador:Let's start with an iterator method:

public static IEnumerable<char> AlphabetSubset(char start, char end)
{
    if (start < 'a' || start > 'z')
        throw new ArgumentOutOfRangeException(paramName: nameof(start), message: "start must be a letter");
    if (end < 'a' || end > 'z')
        throw new ArgumentOutOfRangeException(paramName: nameof(end), message: "end must be a letter");

    if (end <= start)
        throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}");
    for (var c = start; c < end; c++)
        yield return c;
}

Examine el código siguiente que llama al método de iterador incorrectamente:Examine the code below that calls the iterator method incorrectly:

var resultSet = Iterator.AlphabetSubset('f', 'a');
Console.WriteLine("iterator created");
foreach (var thing in resultSet)
    Console.Write($"{thing}, ");

La excepción se inicia cuando se itera resultSet, no cuando se crea resultSet.The exception is thrown when resultSet is iterated, not when resultSet is created. En este ejemplo independiente, la mayoría de los desarrolladores diagnosticarían en seguida el problema.In this contained example, most developers could quickly diagnose the problem. Pero, en bases de datos de mayor tamaño, el código que crea un iterador no suele estar tan cerca del código que enumerar el resultado.However, in larger codebases, the code that creates an iterator often isn't as close to the code that enumerates the result. Puede refactorizar el código para que el método público valide todos los argumentos y un método privado genere la enumeración:You can refactor the code so that the public method validates all arguments, and a private method generates the enumeration:

public static IEnumerable<char> AlphabetSubset2(char start, char end)
{
    if (start < 'a' || start > 'z')
        throw new ArgumentOutOfRangeException(paramName: nameof(start), message: "start must be a letter");
    if (end < 'a' || end > 'z')
        throw new ArgumentOutOfRangeException(paramName: nameof(end), message: "end must be a letter");

    if (end <= start)
        throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}");
    return alphabetSubsetImplementation(start, end);
}

private static IEnumerable<char> alphabetSubsetImplementation(char start, char end)
{ 
    for (var c = start; c < end; c++)
        yield return c;
}

Esta versión refactorizada iniciará excepciones inmediatamente porque el método público no es un método de iterador; solo el método privado usa la sintaxis yield return.This refactored version will throw exceptions immediately because the public method is not an iterator method; only the private method uses the yield return syntax. Pero hay posibles problemas con esta refactorización.However, there are potential problems with this refactoring. El método privado solo debe llamarse desde el método de interfaz público, ya que de lo contrario se omiten todas las validaciones de argumento.The private method should only be called from the public interface method, because otherwise all argument validation is skipped. Los lectores de la clase deben detectar este hecho al leer toda la clase y buscar cualquier otra referencia al método alphabetSubsetImplementation.Readers of the class must discover this fact by reading the entire class and searching for any other references to the alphabetSubsetImplementation method.

Puede dejar más clara la intención del diseño declarando la alphabetSubsetImplementation como función local dentro del método API público:You can make that design intent more clear by declaring the alphabetSubsetImplementation as a local function inside the public API method:

public static IEnumerable<char> AlphabetSubset3(char start, char end)
{
    if (start < 'a' || start > 'z')
        throw new ArgumentOutOfRangeException(paramName: nameof(start), message: "start must be a letter");
    if (end < 'a' || end > 'z')
        throw new ArgumentOutOfRangeException(paramName: nameof(end), message: "end must be a letter");

    if (end <= start)
        throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}");

    return alphabetSubsetImplementation();

    IEnumerable<char> alphabetSubsetImplementation()
    {
        for (var c = start; c < end; c++)
            yield return c;
    }
}

La versión anterior deja claro que hace referencia al método local solo en el contexto del método externo.The version above makes it clear that the local method is referenced only in the context of the outer method. Las reglas para las funciones locales también garantizan que un desarrollador no pueda llamar accidentalmente a la función local desde otra ubicación de la clase y omitir la validación del argumento.The rules for local functions also ensure that a developer can't accidentally call the local function from another location in the class and bypass the argument validation.

La misma técnica se puede emplear con métodos async para asegurarse de que las excepciones derivadas de la validación de argumentos se inician antes de comenzar el trabajo asincrónico:The same technique can be employed with async methods to ensure that exceptions arising from argument validation are thrown before the asynchronous work begins:

public Task<string> PerformLongRunningWork(string address, int index, string name)
{
    if (string.IsNullOrWhiteSpace(address))
        throw new ArgumentException(message: "An address is required", paramName: nameof(address));
    if (index < 0)
        throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be non-negative");
    if (string.IsNullOrWhiteSpace(name))
        throw new ArgumentException(message: "You must supply a name", paramName: nameof(name));

    return longRunningWorkImplementation();

    async Task<string> longRunningWorkImplementation()
    {
        var interimResult = await FirstWork(address);
        var secondResult = await SecondStep(index, name);
        return $"The results are {interimResult} and {secondResult}. Enjoy.";
    }
}

Nota

Algunos de los diseños que se admiten con funciones locales también se podrían realizar con expresiones lambda.Some of the designs that are supported by local functions could also be accomplished using lambda expressions. Aquellos que estén interesados pueden obtener más información sobre las diferenciasThose interested can read more about the differences

Más miembros con forma de expresiónMore expression-bodied members

En C# 6 se presentaron los miembros con forma de expresión para funciones de miembros y propiedades de solo lectura.C# 6 introduced expression-bodied members for member functions, and read-only properties. C# 7.0 amplía los miembros permitidos que pueden implementarse como expresiones.C# 7.0 expands the allowed members that can be implemented as expressions. En C# 7.0, se pueden implementar constructores, finalizadores y descriptores de acceso get y set en propiedades e indizadores.In C# 7.0, you can implement constructors, finalizers, and get and set accessors on properties and indexers. En el código siguiente se muestran ejemplos de cada uno:The following code shows examples of each:

// Expression-bodied constructor
public ExpressionMembersExample(string label) => this.Label = label;

// Expression-bodied finalizer
~ExpressionMembersExample() => Console.Error.WriteLine("Finalized!");

private string label;

// Expression-bodied get / set accessors.
public string Label
{
    get => label;
    set => this.label = value ?? "Default label";
}

Nota

En este ejemplo no se requiere un finalizador, pero se muestra para demostrar la sintaxis.This example does not need a finalizer, but it is shown to demonstrate the syntax. No debe implementar un finalizador en la clase salvo que sea necesario para liberar recursos no administrados.You should not implement a finalizer in your class unless it is necessary to release unmanaged resources. También debe plantearse el uso de la clase SafeHandle en lugar de administrar directamente los recursos no administrados.You should also consider using the SafeHandle class instead of managing unmanaged resources directly.

Estas nuevas ubicaciones para los miembros con forma de expresión representan un hito importante para el lenguaje C#: miembros de la comunidad que trabajan en el proyecto Roslyn de código abierto implementaron estas características.These new locations for expression-bodied members represent an important milestone for the C# language: These features were implemented by community members working on the open-source Roslyn project.

Cambiar un método a un miembro con cuerpo de expresión es un cambio compatible con un elemento binario.Changing a method to an expression bodied member is a binary compatible change.

Expresiones throwThrow expressions

En C#, throw siempre ha sido una instrucción.In C#, throw has always been a statement. Dado que throw es una instrucción, no una expresión, había construcciones de C# en las que no se podía usar.Because throw is a statement, not an expression, there were C# constructs where you could not use it. Incluyen expresiones condicionales, expresiones de fusión nulas y algunas expresiones lambda.These included conditional expressions, null coalescing expressions, and some lambda expressions. La incorporación de miembros con forma de expresión agrega más ubicaciones donde las expresiones throw resultarían útiles.The addition of expression-bodied members adds more locations where throw expressions would be useful. Para que pueda escribir cualquiera de estas construcciones, C# 7.0 presenta las expresiones throw.So that you can write any of these constructs, C# 7.0 introduces throw expressions.

La sintaxis es la misma que siempre ha usado con instrucciones throw.The syntax is the same as you've always used for throw statements. La única diferencia es que ahora puede colocarlas en nuevas ubicaciones, como en una expresión condicional:The only difference is that now you can place them in new locations, such as in a conditional expression:

public string Name
{
    get => name;
    set => name = value ?? 
        throw new ArgumentNullException(paramName: nameof(value), message: "New name must not be null");
}

Esta característica permite usar expresiones throw en expresiones de inicialización:This features enables using throw expressions in initialization expressions:

private ConfigResource loadedConfig = LoadConfigResourceOrDefault() ?? 
    throw new InvalidOperationException("Could not load config");

Anteriormente, esas inicializaciones tendrían que estar en un constructor, con las instrucciones throw en el cuerpo del constructor:Previously, those initializations would need to be in a constructor, with the throw statements in the body of the constructor:

public ApplicationOptions()
{
    loadedConfig = LoadConfigResourceOrDefault();
    if (loadedConfig == null)
        throw new InvalidOperationException("Could not load config");

}

Nota

Dos de las construcciones anteriores iniciarán excepciones durante la construcción de un objeto.Both of the preceding constructs will cause exceptions to be thrown during the construction of an object. Suele ser difícil recuperarse de ellas.Those are often difficult to recover from. Por ese motivo, no se recomienda usar diseños que inicien excepciones durante la construcción.For that reason, designs that throw exceptions during construction are discouraged.

Tipos de valor devueltos de async generalizadosGeneralized async return types

La devolución de un objeto Task desde métodos asincrónicos puede presentar cuellos de botella de rendimiento en determinadas rutas de acceso.Returning a Task object from async methods can introduce performance bottlenecks in certain paths. Task es un tipo de referencia, por lo que su uso implica la asignación de un objeto.Task is a reference type, so using it means allocating an object. En los casos en los que un método declarado con el modificador async devuelva un resultado en caché o se complete sincrónicamente, las asignaciones adicionales pueden suponer un costo considerable de tiempo en secciones críticas para el rendimiento del código.In cases where a method declared with the async modifier returns a cached result, or completes synchronously, the extra allocations can become a significant time cost in performance critical sections of code. Esas asignaciones pueden resultar muy costosas si se producen en bucles ajustados.It can become very costly if those allocations occur in tight loops.

La nueva característica de lenguaje significa que los métodos asincrónicos pueden devolver otros tipos además de Task, Task<T> y void.The new language feature means that async methods may return other types in addition to Task, Task<T> and void. El tipo devuelto debe seguir cumpliendo con el patrón asincrónico, lo que significa que debe haber un método GetAwaiter accesible.The returned type must still satisfy the async pattern, meaning a GetAwaiter method must be accessible. Como ejemplo concreto, se ha agregado el tipo ValueTask a .NET Framework para sacar partido de esta nueva característica del lenguaje:As one concrete example, the ValueTask type has been added to the .NET framework to make use of this new language feature:

public async ValueTask<int> Func()
{
    await Task.Delay(100);
    return 5;
}

Nota

Debe agregar el paquete de NuGet System.Threading.Tasks.Extensions para poder usar el tipo ValueTask<TResult>.You need to add the NuGet package System.Threading.Tasks.Extensions in order to use the ValueTask<TResult> type.

Una optimización simple sería usar ValueTask en lugares donde antes se usaría Task.A simple optimization would be to use ValueTask in places where Task would be used before. Pero si quiere realizar otras optimizaciones a mano, puede almacenar en caché los resultados del trabajo asincrónico y volver a usar el resultado en llamadas posteriores.However, if you want to perform extra optimizations by hand, you can cache results from async work and reuse the result in subsequent calls. La estructura ValueTask tiene un constructor con un parámetro Task para que se pueda diseñar un ValueTask a partir del valor devuelto de cualquier método asincrónico existente:The ValueTask struct has a constructor with a Task parameter so that you can construct a ValueTask from the return value of any existing async method:

public ValueTask<int> CachedFunc()
{
    return (cache) ? new ValueTask<int>(cacheResult) : new ValueTask<int>(LoadCache());
}
private bool cache = false;
private int cacheResult;
private async Task<int> LoadCache()
{
    // simulate async work:
    await Task.Delay(100);
    cacheResult = 100;
    cache = true;
    return cacheResult;
}

Como con todas las recomendaciones de rendimiento, debe evaluar las dos versiones antes de realizar grandes cambios de escala en el código.As with all performance recommendations, you should benchmark both versions before making large scale changes to your code.

Cuando el valor devuelto es el destino de una instrucción await, cambiar una API de Task<TResult> a ValueTask<TResult> es un cambio compatible con el origen.When the return value is the target of an await statement, changing an API from a Task<TResult> to a ValueTask<TResult> is a source compatible change. En general, cambiar a ValueTask no lo es.In general, changing to ValueTask is not.

Mejoras en la sintaxis de literales numéricosNumeric literal syntax improvements

La lectura incorrecta de constantes numéricas puede complicar la comprensión del código cuando se lee por primera vez.Misreading numeric constants can make it harder to understand code when reading it for the first time. Esto suele ocurrir cuando estos números se usan como máscaras de bits u otros elementos simbólicos y no como valores numéricos.This often occurs when those numbers are used as bit masks or other symbolic rather than numeric values. C# 7.0 incluye dos nuevas características que hacen que resulte más fácil escribir números de la manera más legible para el uso previsto: literales binarios y separadores de dígitos.C# 7.0 includes two new features to make it easier to write numbers in the most readable fashion for the intended use: binary literals, and digit separators.

En aquellos casos en que cree máscaras de bits, o siempre que una representación binaria de un número aumente la legibilidad del código, escriba el número en formato binario:For those times when you are creating bit masks, or whenever a binary representation of a number makes the most readable code, write that number in binary:

public const int One =  0b0001;
public const int Two =  0b0010;
public const int Four = 0b0100;
public const int Eight = 0b1000;

El 0b al principio de la constante indica que el número está escrito como número binario.The 0b at the beginning of the constant indicates that the number is written as a binary number.

Los números binarios pueden ser muy largos, por lo que a menudo resulta más fácil ver los patrones de bits introduciendo el _ como separador de dígitos:Binary numbers can get very long, so it's often easier to see the bit patterns by introducing the _ as a digit separator:

public const int Sixteen =   0b0001_0000;
public const int ThirtyTwo = 0b0010_0000;
public const int SixtyFour = 0b0100_0000;
public const int OneHundredTwentyEight = 0b1000_0000;

El separador de dígitos puede aparecer en cualquier parte de la constante.The digit separator can appear anywhere in the constant. En números de base 10, sería habitual usarlo como separador de miles:For base 10 numbers, it would be common to use it as a thousands separator:

public const long BillionsAndBillions = 100_000_000_000;

El separador de dígitos también puede usarse con tipos decimal, float y double:The digit separator can be used with decimal, float and double types as well:

public const double AvogadroConstant = 6.022_140_857_747_474e23;
public const decimal GoldenRatio = 1.618_033_988_749_894_848_204_586_834_365_638_117_720_309_179M;

En conjunto, se pueden declarar constantes numéricas con mucha más legibilidad.Taken together, you can declare numeric constants with much more readability.