Junho de 2018

Volume 33 Número 6

.NET framework - tupla problemas: Por que o c# tuplas obtém dividir as diretrizes

Por Michaelis marca | De 2018 junho

Volta na edição de agosto de 2017 da MSDN Magazine escrevi um artigo detalhado sobre c# 7.0 e o suporte de tuplas (msdn.com/magazine/mt493248). No momento encobri o fato de que o tipo de tupla introduzidas com quebras de C# 7.0 (internamente do tipo ValueTuple <>...) diversas diretrizes de um tipo de valor bem estruturados, ou seja:

• Não declarar campos público ou protegido (em vez disso, encapsular com uma propriedade).

• Não definem os tipos de valor mutável.

• Não criar tipos de valor maior que 16 bytes de tamanho.

Essas diretrizes estão em vigor desde c# 1.0, e ainda aqui no c# 7.0, eles já foram lançados para o vento para definir o tipo de dados System.ValueTuple <>.... Tecnicamente, System.ValueTuple <>... é uma família de tipos de dados de mesmo nome, mas de arity variado (especificamente, o número de parâmetros de tipo). O que é tão especial sobre esse tipo de dados específico que estas diretrizes respeitado longa não se aplicarão mais? E como podemos nossa compreensão das circunstâncias em que essas diretrizes se aplicam – ou não se aplicam – ajude-na aprimorar seus aplicativos para definir os tipos de valor?

Vamos começar a discussão com um foco no encapsulamento e os benefícios das propriedades e campos. Considere, por exemplo, um tipo de valor de arco que representa uma parte entre a circunferência de um círculo. Ele é definido pelo raio do círculo, o ângulo inicial (em graus) do primeiro ponto em arco e o ângulo de flecha (em graus) do último ponto no arco, conforme mostrado no Figura 1.

Figura 1 definindo um 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);
}

Não declarar campos que são públicos ou protegidos

Nesta declaração, o arco é um tipo de valor (definido usando a palavra-chave struct) com três campos públicos que definem as características do arco. Sim, eu poderia ter usado propriedades, mas decidi usar campos públicos neste exemplo especificamente porque viola a diretriz primeiro — não declarar campos público ou protegido.

Aproveitando a campos públicos em vez de propriedades, a definição de arco não tem o mais básico dos princípios de design orientado a objeto — encapsulamento. Por exemplo, se eu decidi alterar a estrutura de dados interno para usar o raio, iniciar o comprimento de ângulo e arco, por exemplo, em vez do ângulo de flecha? Isso obviamente interrompe a interface para arco e todos os clientes seriam forçados a alterar um código.

Da mesma forma, com as definições de Radius, StartAngle e SweepAngle, não ter nenhuma validação. Por exemplo, RADIUS, que pode ser atribuído um valor negativo. E, enquanto valores negativos para StartAngle e SweepAngle podem ser permitidos, não um valor maior que 360 graus. Infelizmente, como arco é definido usando campos públicos, não é possível adicionar validação para proteger contra esses valores. Sim, pode adicionar validação na versão 2, alterando os campos de propriedades, mas fazer violaria a compatibilidade de versão da estrutura do arco. Qualquer código que invocou os campos compilado interrompe em tempo de execução, como seria qualquer código (mesmo se recompilados) que passa o campo como um parâmetro ref.

Dada a orientação que os campos não devem ser público ou protegido, vale a pena observar que propriedades, especialmente com valores padrão, ficaram mais fácil definir que campos explícitos encapsulados por propriedades, graças ao suporte em c# 6.0 para inicializadores de propriedade. Por exemplo, este código:

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

é mais simples do que isso:

private double _SweepAngle = 180;

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

O suporte de inicializador de propriedade é importante porque, sem ele, uma propriedade implementada automaticamente que requer inicialização precisaria de um construtor que o acompanha. Como resultado, a orientação: Torna a (campos particulares mesmo) "Considere propriedades implementadas automaticamente em relação aos campos" sentido, ambos porque o código é mais conciso e porque você não poderá mais modificar campos de fora de sua propriedade recipiente. Tudo isso favorece outra diretriz, "Evite acessar campos de fora de suas propriedades que contêm," que enfatiza o princípio de encapsulamento de dados anteriores descrito até mesmo de outros membros de classe.

Agora permite retornar para o tipo de tupla c# 7.0 ValueTuple <>.... Apesar da orientação sobre campos expostos, ValueTuple < T1, T2 >, por exemplo, é definido da seguinte maneira:

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

O que torna ValueTuple <> … especial? Ao contrário da maioria das estruturas de dados, a tupla c# 7.0, daqui em diante chamada de tupla, não era sobre todo o objeto (como uma pessoa ou CardDeck objeto). Em vez disso, era sobre as partes individuais arbitrariamente agrupadas para fins de transporte, para que eles podem ser retornados de um método sem o incômodo de usar out ou parâmetros ref. Mads Torgersen usa a analogia de um grupo de pessoas que estejam no mesmo barramento — onde o barramento é como uma coleção de itens e as pessoas são como os itens na tupla. Os itens são agrupados em um parâmetro de retorno de tupla porque eles são todos destinados para retornar para o chamador não porque eles têm necessariamente qualquer outra associação uns aos outros. Na verdade, é provável que o chamador, em seguida, recuperar os valores de tupla e trabalhar com elas, individualmente, em vez de como uma unidade.

A importância de itens individuais em vez de todo o torna o conceito de encapsulamento menos interessantes. Considerando que os itens em uma tupla podem ser totalmente relacionados entre si, não é geralmente necessário encapsulá-los de modo que a alteração Item1, por exemplo, pode afetar Item2. (Por outro lado, alterando o comprimento do arco exigiria uma alteração em um ou ambos os ângulos para que encapsulamento é obrigatória.) Além disso, não há nenhum valor inválido para os itens armazenados em uma tupla. Nenhuma validação deve ser imposta no tipo de dados do próprio item, não na atribuição de uma das propriedades do Item na tupla.

Por esse motivo, propriedades na tupla não fornecerem nenhum valor e não há nenhum valor futuro possíveis que podem fornecer. Em resumo, se você pretende definir um tipo de dados são mutáveis sem a necessidade de validação, você também pode usar campos. Outro motivo, que talvez você queira aproveitar propriedades é ter acessibilidade diferentes entre o getter e setter. No entanto, supondo que Mutabilidade é aceitável, você não precisa para tirar proveito das propriedades com acessibilidade getter/setter diferentes, ou. Tudo isso gera outra pergunta — deve o tipo de tupla ser mutável?

Não defina tipos de valor mutável

A orientação próxima a considerar é que o tipo de valor mutável. Novamente, o exemplo de arco (mostrado no código da Figura 2) viola a orientação. É óbvio se você pensar nisso — um tipo de valor passa uma cópia, portanto, alterar a cópia não estará observável do chamador. No entanto, enquanto o código em Figura 2 demonstra o conceito de modificar somente cópia, a legibilidade do código não. De uma perspectiva de legibilidade, aparentemente, as alterações do arco.

Figura 2 tipos de valor são copiados para que o chamador não observa a alteração

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

O que é confuso é que, para que um desenvolvedor espera que o comportamento de cópia do valor, eles teria de saber que arco era de um tipo de valor. No entanto, não há nada do código-fonte que indica o comportamento do tipo de valor (embora para ser justo, o Visual Studio IDE mostrará um tipo de valor como uma estrutura se você passar o mouse sobre o tipo de dados). Você talvez poderia argumentar que os programadores c# devem saber valor tipo versus a semântica do tipo de referência, que o comportamento em Figura 2 é esperado. No entanto, considere o cenário de Figura 3quando o comportamento de cópia não é tão óbvio.

Figura 3 tipos de valor mutável comportam inesperado

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

Observe que, apesar de função Rotate de invocação do arco, do arco, na verdade, nunca gira. Por que? Esse comportamento confuso é devido à combinação de dois fatores. Primeiro, o arco é um tipo de valor que faz com que ele deve ser passado por valor em vez de referência. Como resultado, invocando pizza. Arco retorna uma cópia do arco, em vez de retornar a mesma instância do arco foi instanciado no construtor. Isso não será um problema, se não fosse para o segundo fator. A invocação de girar destina-se a modificar a instância do arco armazenado dentro de pizza, mas na verdade, ela modifica a cópia retornada da propriedade arco. E é por isso que temos a diretriz, "Não define os tipos de valor mutável."

Como antes, tuplas no c# 7.0 ignorar essa diretriz e expõe campos públicos que, por definição, verifique ValueTuple <> … mutável. Apesar dessa violação, ValueTuple <> … não apresenta as desvantagens mesmo como o arco. O motivo é que a única maneira de modificar a tupla é por meio do campo de Item. No entanto, o compilador c# não permite a modificação de um campo (ou propriedade) retornado de um tipo de recipiente (se o tipo de conteúdo é um tipo de referência, tipo de valor ou até mesmo uma matriz ou outro tipo de coleção). Por exemplo, o código a seguir não será compilado:

pie.Arc.Radius = 0;

Nem será este código:

pie.Arc.Radius++;

Essas instruções falham com a mensagem "Erro CS1612: Não é possível modificar o valor de retorno de 'PieShape.Arc' porque ele não é uma variável." Em outras palavras, a orientação não é necessariamente precisa. Em vez de evitar todos os tipos de valor mutável, a chave é evitar a mutação de funções (Propriedades de leitura/gravação são permitidas). Que sabedoria, obviamente, presume que a semântica de valor mostrada no Figura 2 são bastante óbvia, de modo que o comportamento do tipo de valor intrínseco é esperado.

Tipos de valor não crie mais de 16 Bytes

Esta diretriz é necessária devido a frequência na qual o tipo de valor é copiado. Na verdade, com exceção de uma ref ou out parâmetro, tipos de valor são copiados praticamente toda vez que eles são acessados. Isso é verdadeiro se a atribuição de uma instância de tipo de valor para outro (como arco = arco em Figura 3) ou uma invocação de método (como Modify(arc) mostrado em Figura 2). Por motivos de desempenho, a diretriz é manter o tamanho do tipo de valor pequenos.

A realidade é que o tamanho de um ValueTuple <>... pode frequentemente ser maior que 128 bits (16 bytes) porque um ValueTuple <>... pode conter sete itens individuais (e ainda mais se você especificar outra tupla para o parâmetro de tipo oitavo). Por que, em seguida, é a tupla c# 7.0 definida como um tipo de valor?

Como mencionado anteriormente, a tupla foi introduzida como um recurso de linguagem para permitir vários valores de retorno sem a sintaxe complexa exigida pela out ou ref parâmetros. O padrão geral, em seguida, era construir e retornar uma coleção de itens e, em seguida, decompor-lo de volta ao chamador. Na verdade, passar uma coleção de itens para baixo na pilha por meio de um parâmetro de retorno é semelhante a passar um grupo de argumentos na pilha para uma chamada de método. Em outras palavras, tuplas de retorno são simétricas com listas de parâmetros individuais que diz respeito a cópias de memória.

Se você declarou que a tupla como um tipo de referência, seria necessário construir o tipo no heap e inicializá-lo com os valores de Item — copiando potencialmente um valor ou uma referência para o heap. De qualquer forma, uma operação de cópia de memória é necessário, semelhante de cópia de memória de um tipo de valor. Além disso, em algum momento posterior no tempo em que a tupla de referência não é mais acessível, o coletor de lixo precisará recuperar a memória. Em outras palavras, uma tupla de referência ainda envolve a cópia de memória, bem como pressão adicional no coletor de lixo, tornando a opção mais eficiente de uma tupla de tipo de valor. (Em casos raros, os que uma tupla de valor não é mais eficiente, você pode ainda recorrer a versão do tipo de referência, tupla <>....)

Enquanto completamente ortogonal para o tópico principal do artigo, observe a implementação de Equals e GetHashCode em Figura 1. Você pode ver como tuplas fornecem um atalho para a implementação de Equals e GetHashCode. Para obter mais informações, consulte "Usando tuplas a igualdade de substituição e GetHashCode".

Conclusão

À primeira vista pode parecer surpreendente de tuplas a ser definido como tipos de valor imutável. Afinal, o número de tipos de valor imutável encontrado no .NET Core e o .NET Framework é mínimo e há duradoura diretrizes que chamam para tipos de valor a ser encapsulado com propriedades e imutável de programação. Também há a influência da característica abordagem imutável por padrão para F #, que pressionadas designers de linguagem c# para fornecer uma abreviação para declarar variáveis imutáveis ou definir tipos imutáveis. (Embora nenhum tal abreviada está atualmente em questão para c# 8.0, estruturas de somente leitura foram adicionadas ao C# 7.2 como um meio para verificar se uma struct foi imutável.)

No entanto, quando você investigar em detalhes, verá uma série de fatores importantes. Estão incluídos:

Tipos de referência • impõem um impacto de desempenho adicionais com a coleta de lixo.

• Tuplas são geralmente efêmeras.

• Tupla itens não possuem previstas encapsulamento com propriedades.

• Tuplas mesmo grandes (por diretrizes de tipo de valor) não tem operações de cópia de memória significativa além de uma implementação de referência de tupla.

Em resumo, há muitos fatores que favorecem uma tupla de tipo de valor com campos públicos, apesar das diretrizes padrão. No final, diretrizes são apenas esse, diretrizes. Não ignorá-las, mas dado suficiente — e explicitamente sugiro, documentadas — causa, é Okey para colorir fora das linhas na ocasião.

Para obter mais informações sobre as diretrizes para definir os tipos de valor e substituir Equals e GetHashCode, confira capítulos 9 e 10 no meu livro essencial c#: "Essencial c# 7.0" (IntelliTect.com/EssentialCSharp), que deve ser em maio.


Mark Michaelis é fundador da IntelliTect, onde atua como arquiteto técnico principal e instrutor. Por quase duas décadas ele foi um MVP da Microsoft e foi Diretor Regional da Microsoft desde 2007. Michaelis serve no design de software Microsoft várias examine equipes, incluindo c#, Azure Mi crosoft, SharePoint e Visual Studio ALM. Ele participa de conferências de desenvolvedor e escreveu diversos livros, incluindo sua mais recente, "Es sential c# 6.0 (5 de edição)" (itl.tc/EssentialCSharp). Você pode contatá-lo pelo Facebook, em facebook.com/Mark.Michaelis, pelo seu blog IntelliTect.com/Mark, no Twitter: @markmichaelis ou pelo email mark@IntelliTect.com.