Junio de 2018

Volumen 33; número 6

.NET framework - tupla problemas: ¿Por qué obtener tuplas de C# interrumpir las instrucciones

Por Michaelis marca | De 2018 de junio

Nuevo en el número de agosto de 2017 de MSDN Magazine he escrito un artículo detallado en C# 7.0 y su compatibilidad con tuplas (msdn.com/magazine/mt493248). En el momento por alto por el hecho de que el tipo de tupla introducido con saltos de C# 7.0 (internamente de tipo ValueTuple <>...) varias instrucciones de un tipo de valor estructurado, a saber:

• No declarar campos que son públicos o protegidos (en su lugar se encapsulan con una propiedad).

• No define los tipos de valor mutable.

• No cree los tipos de valor superior a 16 bytes de tamaño.

Estas instrucciones se han en su lugar desde C# 1.0, y aún aquí en C# 7.0, ha ha producido para el viento para definir el tipo de datos System.ValueTuple <> …. Técnicamente, System.ValueTuple <>... es una familia de tipos de datos del mismo nombre pero de distintos aridad (específicamente, el número de parámetros de tipo). ¿Novedades de este tipo de datos determinado que ya no se aplican estas directrices respeta larga especial? ¿Y cómo puede nuestra comprensión de las circunstancias en las que se aplican estas instrucciones, o no son aplicables: nos ayudarán a refinar su aplicación a la definición de tipos de valor?

Comencemos la explicación haciendo hincapié en la encapsulación y las ventajas de propiedades frente a campos. Considere, por ejemplo, un tipo de valor de arco que representa una parte de la circunferencia de un círculo. Se define por el radio del círculo, el ángulo inicial (en grados) del primer punto en el arco y el ángulo de barrido (en grados) del último punto en el arco, tal y como se muestra en figura 1.

Figura 1 define un arco

public struct Arc
{
  public Arc (double radius, double startAngle, double sweepAngle)
  {
    Radius = radius;
    StartAngle = startAngle;
    SweepAngle = sweepAngle;
  }

  public double Radius;
  public double StartAngle;
  public double SweepAngle;

  public double Length
  {
    get
    {
      return Math.Abs(StartAngle - SweepAngle)
        / 360 * 2 * Math.PI * Radius;
    }
  }

  public void Rotate(double degrees)
  {
    StartAngle += degrees;
    SweepAngle += degrees;
  }

  // Override object.Equals
  public override bool Equals(object obj)
  {
    return (obj is Arc)
      && Equals((Arc)obj);
  }

        // Implemented IEquitable<T>
  public bool Equals(Arc arc)
  {
    return (Radius, StartAngle, SweepAngle).Equals(
      (arc.Radius, arc.StartAngle, arc.SweepAngle));
  }

  // Override object.GetHashCode
  public override int GetHashCode() =>
    return (Radius, StartAngle, SweepAngle).GetHashCode();

  public static bool operator ==(Arc lhs, Arc rhs) =>
    lhs.Equals(rhs);

  public static bool operator !=(Arc lhs, Arc rhs) =>
    !lhs.Equals(rhs);
}

No se declara campos que son públicos o protegidos

En esta declaración, arco es un tipo de valor (definido mediante la palabra clave struct) con tres campos públicos que definen las características del arco. Sí, podría haber usado propiedades, pero decide usar campos públicos en este ejemplo concreto porque infringe el primer criterio: no declarar campos que son públicos o protegidos.

Mediante el aprovechamiento de los campos públicos, en lugar de propiedades, la definición de arco no tiene la más básica de principios de diseño orientado a objetos: encapsulación. Por ejemplo, ¿qué ocurre si decide cambiar la estructura de datos interna para usar el radio, iniciar la longitud del arco y ángulo, por ejemplo, en lugar de un ángulo de barrido? Si lo hace, obviamente, se interrumpiría la interfaz arco y todos los clientes se ve obligados a realizar un código de cambio.

De forma similar, con las definiciones de Radius, StartAngle y SweepAngle, no tengo ninguna validación. RADIUS, por ejemplo, podría tener asignado un valor negativo. Y, aunque los valores negativos para parámetros StartAngle y SweepAngle pueden estar permitidos, no tendría un valor superior a 360 grados. Lamentablemente, como arco se define mediante campos públicos, no hay ninguna manera de agregar validación para protegerse frente a estos valores. Sí, podría agregar la validación de la versión 2, cambie los campos a las propiedades, pero al hacerlo interrumpiría la compatibilidad de versión de la estructura del arco. Cualquier existente compila código que invoca los campos se interrumpiría en tiempo de ejecución, como haría cualquier código (incluso si vuelve a compilar) que pasa el campo como un parámetro ref.

Proporciona las directrices que los campos no deben ser público o protegido, merece la pena indicar que propiedades, sobre todo con valores predeterminados, pasara a ser más fáciles de definir que los campos explícitos encapsulados por propiedades, gracias a la compatibilidad con en C# 6.0 inicializadores de propiedades. Este código, por ejemplo:

public double SweepAngle { get; set; } = 180;

es más sencillo que esto:

private double _SweepAngle = 180;

public double SweepAngle {
  get { return _SweepAngle; }
  set { _SweepAngle = value; }
}

La compatibilidad de inicializador de propiedad es importante porque, sin él, una propiedad implementada automáticamente que debe inicializarse tendría un constructor que lo acompaña. Como resultado, la instrucción: "Considere la posibilidad de propiedades implementadas automáticamente a través de los campos" hace de (campos privados incluso) tienen sentido, ambos porque el código es más conciso y porque ya no puede modifican campos de fuera de su propiedad que lo contiene. Todo esto favorece otra regla, "Evitar el acceso a campos desde fuera de sus propiedades que lo contiene," que resalta el principio de encapsulación de datos se describe anteriormente incluso de otros miembros de clase.

En este momento permite devolver para el tipo de tupla de C# 7.0 ValueTuple <> …. A pesar de las directrices acerca de los campos expuestos, ValueTuple < T1, T2 >, por ejemplo, se define como sigue:

public struct ValueTuple<T1, T2>
  : IComparable<ValueTuple<T1, T2>>, ...
{
  public T1 Item1;
  public T2 Item2;
  // ...
}

¿Qué hace especial ValueTuple <>...? A diferencia de la mayoría de las estructuras de datos, la tupla 7.0 de C#, que ahora en adelante se denomina tupla, no era sobre todo el objeto (por ejemplo, una persona o CardDeck un objeto). En su lugar, era acerca de las partes individuales arbitrariamente agrupadas con fines de transporte, por lo que pudieron devolverse desde un método sin la molestia de usando out o parámetros ref. Mads Torgersen usa la analogía de un grupo de personas que deben estar en el mismo bus, donde el bus es como una tupla y las personas son similares a los elementos de la tupla. Los elementos se agrupan en un parámetro de valor devuelto de tupla porque están todos destinados a devolver al llamador, no porque tienen necesariamente cualquier otra asociación entre sí. De hecho, es probable que el autor de la llamada, a continuación, se recuperan los valores de la tupla y trabajar con ellos de forma individual en lugar de como una unidad.

La importancia de los elementos individuales en lugar de todo el hace que el concepto de encapsulación menos interesantes. Dado que los elementos de una tupla pueden ser totalmente no relacionados entre sí, a menudo es necesario para colocarlas de tal forma que cambiar Item1, por ejemplo, podría afectar a Item2. (Por el contrario, cambiar la longitud del arco requeriría un cambio en uno o ambos de los ángulos para que encapsulación es un requisito indispensable.) Además, no hay ningún valor no válido para los elementos almacenados en una tupla. Se forzaría ninguna validación en el tipo de datos del elemento, no en la asignación de una de las propiedades del elemento de la tupla.

Por esta razón, propiedades de la tupla no proporcionan ningún valor y no tienen ningún valor futuro plausible que podrían proporcionar. En resumen, si se va a definir un tipo cuyos datos mutables sin necesidad de validación, también puede usar campos. Otro motivo que desea aprovechar propiedades es tener accesibilidad diferentes entre el captador y el establecedor. Sin embargo, suponiendo que mutabilidad es aceptable, si no va a aprovechar las ventajas de propiedades con la accesibilidad de captador y establecedor que no son iguales, o bien. ¿Todo esto genera otra pregunta: el tipo de tupla debe ser mutable?

No se Define los tipos de valor Mutable

La instrucción siguiente para tener en cuenta es el tipo de valor mutable. Una vez más, en el ejemplo de arco (se muestra en el código en figura 2) infringe la regla. Es obvio si piensa sobre él: un tipo de valor pasa una copia, por lo que cambiar la copia no se puede observar desde el llamador. Sin embargo, mientras el código en figura 2 muestra el concepto de sólo modifica la copia, la legibilidad del código no lo hace. Desde una perspectiva de legibilidad, podría parecer los cambios del arco.

Tipos de valor de la figura 2 se copian para que el llamador no observa el cambio

[TestMethod]
public void PassByValue_Modify_ChangeIsLost()
{
  void Modify(Arc paramameter) { paramameter.Radius++; }
  Arc arc = new Arc(42, 0, 90);
  Modify(arc);
  Assert.AreEqual<double>(42, arc.Radius);
}

¿Qué es confuso que es correcto para que un desarrollador puede esperar el comportamiento de copia de valor, tendría que conocer que arco encontró un tipo de valor. Sin embargo, no hay nada obvio desde el código fuente que indica el comportamiento del tipo de valor (aunque sea razonable, el IDE de Visual Studio mostrará un tipo de valor como un struct si mantiene el mouse sobre el tipo de datos). Quizás podría argumentar que los programadores de C# deben saber valor escriba frente a la semántica de tipos de referencia, por ejemplo, que el comportamiento en figura 2 se espera. Sin embargo, considere el escenario en figura 3cuando el comportamiento de copia no es tan obvio.

Figura 3 tipos de valor Mutable se comportan de forma inesperada

public class PieShape
{
  public Point Center { get; }
  public Arc Arc { get; }

  public PieShape(Arc arc, Point center = default)
  {
    Arc = arc;
    Center = center;
  }
}

public class PieShapeTests
{
  [TestMethod]
  public void Rotate_GivenArcOnPie_Fails()
  {
    PieShape pie = new PieShape(new Arc(42, 0, 90));
    Assert.AreEqual<double>(90, pie.Arc.SweepAngle);
    pie.Arc.Rotate(42);
    Assert.AreEqual<double>(90, pie.Arc.SweepAngle);
  }
}

Tenga en cuenta que, a pesar de la función de giro del arco de invocación, el arco, de hecho, nunca se gira. ¿Por qué no? Este comportamiento confuso es debido a la combinación de dos factores. En primer lugar, arco es un tipo de valor que hace que se va a pasar por valor, en lugar de por referencia. Como resultado, invocar circular. Arco devuelve una copia del arco, en lugar de devolver la misma instancia de arco que se creó una instancia en el constructor. Esto no sería un problema si no estaba para el segundo factor. La invocación de girar está diseñada para modificar la instancia de arco almacenado en el gráfico circular, pero en realidad, modifica la copia devuelta por la propiedad del arco. Y por eso tenemos la directriz, "No se define los tipos de valor mutable."

Como antes, tuplas en C# 7.0 omitir esta indicación y expone campos públicos que, por definición, asegúrese de ValueTuple <>... mutable. A pesar de esta infracción, ValueTuple <>... no sufren las mismas desventajas como arco. La razón es que la única manera de modificar la tupla es a través del campo de elemento. Sin embargo, el compilador de C# no permite la modificación de un campo (o propiedad) procedente de un tipo de contenedor (si el tipo contenedor es un tipo de referencia, tipo de valor o incluso una matriz u otro tipo de colección). Por ejemplo, no se compilará el código siguiente:

pie.Arc.Radius = 0;

Ni el código de este código:

pie.Arc.Radius++;

Estas instrucciones no funcionarán con el mensaje "Error CS1612: No se puede modificar el valor devuelto de 'PieShape.Arc' porque no es una variable." En otras palabras, la instrucción no es necesariamente exactos. En lugar de evitar todos los tipos de valor mutable, la clave es evitar mutación de funciones (propiedades de lectura/escritura están permitidas). Que conocimientos, por supuesto, se da por supuesto que la semántica de valor se muestra en figura 2 son lo suficientemente obvio que se espera que el comportamiento del tipo de valor intrínseco.

No crear tipos de valor superior a 16 Bytes

Esta indicación es necesario debido a la frecuencia con la que se copia el tipo de valor. De hecho, con la excepción de un parámetro ref o el parámetro de salida, los tipos de valor se copian en prácticamente cada vez que se está acceder a ellos. Esto es cierto si la asignación de una instancia de tipo de valor a otro (como arco = arco en figura 3) o una invocación de método (como se muestra en el Modify(arc) figura 2). Por motivos de rendimiento, la coordenada es mantener el tamaño del tipo de valor pequeño.

La realidad es que el tamaño de un valor puede ValueTuple <>... suele ser mayor de 128 bits (16 bytes) debido a un ValueTuple <>... puede contener siete elementos individuales (y aún más si se especifica otro tupla del octavo parámetro de tipo). ¿Por qué, a continuación, se la tupla de C# 7.0 define como un tipo de valor?

Como se mencionó anteriormente, la tupla se introdujo como una característica del lenguaje para permitir varios valores devueltos sin la sintaxis compleja necesaria por out o ref parámetros. El patrón general, a continuación, era construir y devolver una tupla y, a continuación, se anular en el llamador. De hecho, pasando una tupla hacia abajo en la pila a través de un parámetro de valor devuelto es similar a pasar un grupo de argumentos de la pila para una llamada al método. En otras palabras, tuplas devueltos son simétricos con listas de parámetros individuales en lo que se refiere a copias de memoria.

Si declara la tupla como tipo de referencia, sería necesario construir el tipo en el montón e inicialícela con los valores de elemento: copiar potencialmente un valor o una referencia al montón. En cualquier caso, una operación de copia de memoria es necesario, de forma similar a la de copia de memoria de un tipo de valor. Además, en algún punto posterior de tiempo cuando la tupla de referencia ya no es accesible, el recolector de elementos no utilizados será necesario recuperar la memoria. En otras palabras, una tupla de referencia todavía implica la copia de memoria, así como una presión adicional en el recolector de elementos no utilizados, siendo la opción más eficaz de una tupla de tipo de valor. (En los casos poco frecuentes que una tupla de valor no es más eficaz, podría todavía recurrir a la versión de tipo de referencia, tupla <> ….)

Mientras completamente ortogonal al tema principal del artículo, tenga en cuenta la implementación de Equals y GetHashCode en figura 1. Puede ver cómo tuplas proporcionan un acceso directo para implementar Equals y GetHashCode. Para obtener más información, consulte "Using tuplas para igualdad de invalidación y GetHashCode".

Resumen

A primera vista, puede parecer sorprendente de tuplas que se definen como tipos de valor inmutable. Después de todo, el número de tipos de valor inmutable situados en .NET Core y .NET Framework es mínimo y existen desde hace mucho tiempo directrices que requieren tipos de valor sea inmutable y encapsulado con propiedades de programación. También es la influencia de la característica de enfoque inmutable de forma predeterminada a F #, que presiones diseñadores del lenguaje C# para proporcionar un método abreviado para declarar variables inmutable o definir tipos inmutables. (Aunque no abreviada de este tipo es actualmente en cuestión para C# 8.0, structs de solo lectura se agregaron a 7.2 de C# como un medio para comprobar que un struct se inmutable.)

Sin embargo, al profundizar en los detalles, verá una serie de factores importantes. Éstos son:

• Tipos de referencia suponer un impacto de rendimiento adicionales con la recolección.

• Tuplas son generalmente efímeros.

Los elementos de tupla • no tienen es necesario para la encapsulación con propiedades previsible.

• Tuplas incluso grandes (de instrucciones de tipos de valor) no tienen operaciones de copia de memoria significativa más allá de una implementación de referencia de la tupla.

En resumen, hay muchos factores que dan prioridad una tupla de tipo de valor con campos públicos a pesar de las instrucciones estándar. Al final, instrucciones son simplemente que, directrices. No puedan omitir, pero no proporciona suficientes e indicaría documentan explícitamente: causa, es perfectamente para colorear fuera de las líneas en alguna ocasión.

Para obtener más información sobre las directrices para definir los tipos de valor y reemplazar Equals y GetHashCode, visite capítulos 9 y 10 en mi libro esencial C#: "Esencial C# 7.0" (IntelliTect.com/EssentialCSharp), que debe ser out en mayo.


Mark Michaelis es el fundador de IntelliTect y trabaja de arquitecto técnico como jefe y formador. Durante casi dos décadas ha sido MVP de Microsoft y ha sido Director Regional de Microsoft desde 2007. Michaelis actúa sobre el diseño de software de Microsoft varias Revisar equipos, como C#, Mi crosoft Azure, SharePoint y Visual Studio ALM. Ofrece conferencias de desarrollador y se ha escrito numerosos libros, incluido su más reciente, "Es sential C# 6.0 (5ª edición)" (itl.tc/EssentialCSharp). Póngase en contacto con él en Facebook en facebook.com/Mark.Michaelis, en su blog IntelliTect.com/Mark, en Twitter @markmichaelis o a través de la dirección de correo electrónico mark@IntelliTect.com.