Escolher identidade e comportamento
Uma classe pode representar uma entidade com identidade própria, como uma sessão ou uma ligação. Duas sessões com os mesmos campos não são necessariamente a mesma sessão. Um valor de dados, como um limite identificado por nome e quantidade, pode ter outra regra: duas instâncias com os mesmos componentes podem representar a mesma informação.
Um record acrescenta comportamento gerado para modelos de dados, incluindo igualdade. Não elimina a distinção entre classe e struct. record sem outra indicação declara um record class, que continua a ser um tipo de referência. Atribuir essa referência a outra variável não cria um snapshot. record struct continua a ser um tipo de valor.
Escolhe primeiro a regra que o consumidor precisa de observar. Se interessa comparar o conteúdo, define exatamente quais componentes fazem parte dessa identidade. Se interessa acompanhar um objeto ao longo de alterações, não confundas igualdade atual de campos com identidade da entidade. Uma palavra-chave reduz código repetitivo, mas não decide o significado dos dados pelo produto.
Despacho virtual e ocultação
Uma variável pode estar declarada como Policy e conter uma instância de RegionalPolicy. Se Policy declara um método virtual e RegionalPolicy faz override, a chamada pela variável Policy usa o override aplicável ao objeto. Assim, o consumidor utiliza um contrato comum sem escolher manualmente cada implementação.
new num membro tem outro significado: oculta um membro herdado. A escolha do membro ocultado depende do tipo da expressão usada na chamada. Se a base tem um método não virtual Name e a derivada declara new Name, chamar pela variável da base continua a usar o membro da base. Trocar override por new apenas para eliminar um erro pode mudar o comportamento dos consumidores existentes.
sealed override impede overrides posteriores desse membro, sem selar necessariamente a classe inteira. Mesmo quando um override é permitido, o comportamento deve continuar a respeitar o contrato. Não reduzas silenciosamente o conjunto de entradas aceites nem declares sucesso com outro significado. Compatibilidade de assinatura é uma condição de compilação; compatibilidade de comportamento exige revisão.
Interfaces e contratos que colidem
Uma interface permite pedir uma capacidade sem exigir uma classe concreta. Um consumidor que recebe IExporter precisa do contrato Export, não de conhecer todos os detalhes internos do adaptador. Isto facilita substituir implementações, desde que as promessas de formato, falha e efeitos continuem iguais.
Duas interfaces podem ter um método com a mesma assinatura e expectativas diferentes. Se ILegacy.Export devolve CSV e IModern.Export devolve JSON, um único método público que devolve sempre JSON não cumpre ambos os contratos. Implementações explícitas, string ILegacy.Export() e string IModern.Export(), permitem associar o comportamento à interface utilizada pelo chamador.
O membro explícito é chamado através da interface. Uma variável do tipo concreto não ganha automaticamente um método público Export com essa implementação. A conversão para a interface não cria um segundo adaptador: muda o contrato acessível sobre o mesmo objeto. Documenta os efeitos e o formato de cada contrato para que a diferença não fique escondida apenas no corpo do método.
Inicializar não é validar
Numa declaração posicional record Allocation(string Region, int Units), as propriedades geradas do record class são init-only. O objeto pode ser inicializado, mas uma atribuição posterior a Region não é permitida dessa forma. Uma variante pode ser criada com with, conservando o original.
required exige que o membro seja inicializado na construção relevante; não prova que o valor tenha significado válido. Com required string? Note, atribuir null cumpre a obrigação de inicialização porque a propriedade admite null. Omitir Note num inicializador que tem de o fornecer continua a ser um erro. Uma cadeia vazia também pode ser atribuída a uma propriedade string sem violar a obrigação required.
As regras do domínio precisam de validação própria. Se Units deve ser positivo ou Region deve pertencer a um conjunto, nem init nem required demonstram esses factos. Mantém essas verificações numa fronteira clara e explica o que acontece quando falham. Usa os modificadores para expressar o contrato de construção, sem os apresentar como validação de negócio automática.
Igualdade e cópia têm limites
Dois record classes construídos separadamente com os mesmos componentes string e int podem comparar iguais com == e continuar a ser objetos distintos para ReferenceEquals. A igualdade gerada também distingue tipos de record em runtime: uma base e uma derivada não passam a ser iguais apenas porque os campos da base coincidem.
Um membro array exige especial atenção. Num record que contém int[], a igualdade do membro não compara automaticamente cada elemento. Dois arrays diferentes com números iguais continuam a ser referências diferentes. Se dois records partilham o mesmo array, a igualdade desse membro pode coincidir e uma edição do array ser visível em ambos.
with faz uma cópia superficial por predefinição. Copia as referências dos membros, sem clonar todos os objetos que elas alcançam. Por isso, init não basta para garantir um histórico imutável. Para preservar um array de strings contra edições de elementos noutro array, separa os contentores e controla quem conserva referências mutáveis. Para elementos que também sejam objetos mutáveis, a política de cópia precisa de considerar esses objetos.
Exemplo guiado: uma política substituível
O exemplo apresenta uma decisão de aceitação de lotes através de uma interface. IAdmission recebe a quantidade e devolve um booleano; FixedAdmission aceita de 1 a 80. O método Plan cria um record Receipt com os dados recebidos e a decisão. Não escreve em filas, não reserva capacidade e não confirma entrega.
Com uma referência IAdmission para FixedAdmission, Plan(policy, 40) produz um Receipt aceite. Com 0 ou 81, Accepted é false. Os dois componentes do record são valores simples; não há um contentor mutável partilhado. É possível construir outro Receipt com os mesmos componentes e obter igualdade de valor, sem que isso represente a mesma execução de negócio.
Esta distinção é importante no trabalho real. Uma decisão local igual não é uma chave de idempotência nem uma prova de execução única. Se o sistema vier a produzir efeitos, precisa de uma identidade operacional, tratamento de falhas e evidência próprios. Aqui, o exercício isola a escolha de implementação pela interface e a representação da decisão.
Rever o contrato observado pelo consumidor
Ao rever uma API, começa pelo tipo declarado do consumidor e pelo tipo real do objeto. Identifica se a chamada usa despacho virtual, ocultação ou implementação de interface. Depois verifica que formato e que efeitos foram prometidos, em vez de assumir que uma assinatura igual garante comportamento igual.
Nos modelos de dados, separa quatro perguntas: o membro tem de ser inicializado, pode ser reatribuído, o objeto alcançado pode mudar e como é comparada a igualdade? required, init, referência readonly e record respondem a partes diferentes deste problema. Um array dentro de um record permite ver por que essas perguntas não são equivalentes.
Resumo: override participa no contrato virtual; new oculta; interfaces explícitas distinguem contratos; required controla inicialização; records acrescentam igualdade e cópia com regras concretas. As perguntas seguintes pedem decisões sobre essas regras e os seus limites, incluindo preservação de histórico e compatibilidade entre consumidores.
public interface IAdmission
{
bool Accept(int units);
}
public sealed class FixedAdmission : IAdmission
{
public bool Accept(int units) => units >= 1 && units <= 80;
}
public record Receipt(int Units, bool Accepted);
public static class Planning
{
public static Receipt Plan(IAdmission policy, int units)
=> new Receipt(units, policy.Accept(units));
}Declarações de tipos. Assume policy não nulo. Planning.Plan(new FixedAdmission(), 40) representa uma decisão aceite; 0 e 81 são rejeitados, sem efeitos externos.
Armadilhas comuns
Trocar override por new; chamar um membro explícito pela classe concreta; tomar required por validação; assumir cópia profunda de arrays num record.
Tópicos relacionados: Referências, nullability e texto · Métodos, parâmetros e padrões · Coleções, generics e igualdade
O contrato inclui comportamento, identidade e propriedade dos dados; as palavras-chave só exprimem partes desse contrato.
Referência: C# record types · C# 14 / .NET 10; no preview features