Membros Obrigatórios
Observação
Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele inclui mudanças de especificação propostas, juntamente com as informações necessárias durante o design e desenvolvimento do recurso. Estes artigos são publicados até que as alterações de especificações propostas sejam finalizadas e incorporadas na especificação ECMA atual.
Pode haver algumas discrepâncias entre a especificação do recurso e a implementação concluída. Essas diferenças são capturadas nas notas pertinentes da Language Design Meeting (LDM).
Você pode saber mais sobre o processo de adoção de especificações de recursos no padrão de linguagem C# no artigo sobre as especificações .
Questão campeã: https://github.com/dotnet/csharplang/issues/3630
Resumo
Esta proposta adiciona uma maneira de especificar que uma propriedade ou campo deve ser definido durante a inicialização do objeto, forçando o criador da instância a fornecer um valor inicial para o membro em um inicializador de objeto no site de criação.
Motivação
Hoje em dia, as hierarquias de objetos exigem muito código repetitivo para transportar dados por todos os níveis da hierarquia. Vejamos uma hierarquia simples envolvendo um Person
como pode ser definido em C# 8:
class Person
{
public string FirstName { get; }
public string MiddleName { get; }
public string LastName { get; }
public Person(string firstName, string lastName, string? middleName = null)
{
FirstName = firstName;
LastName = lastName;
MiddleName = middleName ?? string.Empty;
}
}
class Student : Person
{
public int ID { get; }
public Student(int id, string firstName, string lastName, string? middleName = null)
: base(firstName, lastName, middleName)
{
ID = id;
}
}
Há muita repetição acontecendo aqui:
- Na raiz da hierarquia, o tipo de cada propriedade teve que ser repetido duas vezes, e o nome teve que ser repetido quatro vezes.
- No nível derivado, o tipo de cada propriedade herdada tinha que ser repetido uma vez, e o nome tinha que ser repetido duas vezes.
Esta é uma hierarquia simples com 3 propriedades e 1 nível de herança, mas muitos exemplos do mundo real desses tipos de hierarquias vão muitos níveis mais profundos, acumulando um número cada vez maior de propriedades para transmitir à medida que o fazem. Roslyn é uma dessas bases de código, por exemplo, nos vários tipos de árvore que constituem os nossos CSTs e ASTs. Esse aninhamento é tedioso o suficiente para que tenhamos geradores de código para gerar os construtores e definições desses tipos, e muitos clientes adotam abordagens semelhantes para o problema. O C# 9 introduz registos, o que em alguns cenários poderá melhorar isto:
record Person(string FirstName, string LastName, string MiddleName = "");
record Student(int ID, string FirstName, string LastName, string MiddleName = "") : Person(FirstName, LastName, MiddleName);
record
eliminam a primeira fonte de duplicação, mas a segunda fonte de duplicação permanece inalterada: infelizmente, esta é a fonte de duplicação que cresce à medida que a hierarquia cresce, e é a parte mais dolorosa da duplicação que deve ser corrigida após uma alteração na hierarquia, pois exige perseguir a hierarquia em todos os seus locais, possivelmente até mesmo entre projetos e potencialmente causar problemas aos consumidores.
Como uma solução alternativa para evitar essa duplicação, há muito tempo vemos os consumidores adotando inicializadores de objeto como uma forma de evitar escrever construtores. Antes do C# 9, no entanto, isso tinha 2 grandes desvantagens:
- A hierarquia de objetos deve ser totalmente mutável, com acessadores
set
em cada propriedade. - Não há como garantir que cada instanciação de um objeto do gráfico defina cada membro.
O C# 9 novamente abordou o primeiro problema aqui, introduzindo o acessador init
: com ele, essas propriedades podem ser definidas na criação/inicialização de objetos, mas não posteriormente. No entanto, ainda temos o segundo problema: as propriedades em C# são opcionais desde o C# 1.0. Os tipos de referência anuláveis, introduzidos no C# 8.0, resolveram parte desse problema: se um construtor não inicializar uma propriedade de tipo de referência não anulável, o usuário será avisado sobre isso. No entanto, isto não resolve o problema: o utilizador aqui pretende evitar repetir grandes partes do seu tipo no construtor; ele deseja passar o requisito e para definir propriedades para os seus consumidores. Ele também não fornece nenhum aviso sobre ID
de Student
, pois esse é um tipo de valor. Esses cenários são extremamente comuns em ORMs de modelos de banco de dados, como o EF Core, que precisam ter um construtor público sem parâmetros, mas depois definem a nulabilidade das linhas com base na nulabilidade das propriedades.
A presente proposta procura dar resposta a estas preocupações através da introdução de uma nova funcionalidade para o C#: membros obrigatórios. Os membros necessários deverão ser inicializados pelos consumidores, em vez de pelo autor do tipo, com várias personalizações para permitir flexibilidade para vários construtores e outros cenários.
Design Detalhado
class
, struct
e record
tipos ganham a capacidade de declarar uma required_member_list. Esta lista é a lista de todas as propriedades e campos de um tipo que são considerados necessáriose devem ser inicializados durante a construção e inicialização de uma instância do tipo. Os tipos herdam essas listas de seus tipos base automaticamente, proporcionando uma experiência perfeita que remove códigos clichês e repetitivos.
required
modificador
Adicionamos 'required'
à lista de modificadores em field_modifier e property_modifier. O required_member_list de um tipo é composto por todos os membros que tiveram required
aplicado a si. Assim, o tipo Person
de anteriormente agora fica assim:
public class Person
{
// The default constructor requires that FirstName and LastName be set at construction time
public required string FirstName { get; init; }
public string MiddleName { get; init; } = "";
public required string LastName { get; init; }
}
Todos os construtores de um tipo que tem um required_member_list anunciam automaticamente um contrato que exige que os consumidores do tipo inicializem todas as propriedades na lista. É um erro para um construtor anunciar um contrato que requer um membro que não é pelo menos tão acessível quanto o próprio construtor. Por exemplo:
public class C
{
public required int Prop { get; protected init; }
// Advertises that Prop is required. This is fine, because the constructor is just as accessible as the property initer.
protected C() {}
// Error: ctor C(object) is more accessible than required property Prop.init.
public C(object otherArg) {}
}
required
só é válido nos tipos class
, struct
e record
. Não é válido para tipos interface
.
required
não pode ser combinado com os seguintes modificadores:
fixed
ref readonly
ref
const
static
required
não pode ser aplicado a indexadores.
O compilador emitirá um aviso quando Obsolete
for aplicado a um membro necessário de um tipo e:
- O tipo não foi marcado como
Obsolete
, ou - Qualquer construtor que não seja atribuído com
SetsRequiredMembersAttribute
não está marcadoObsolete
.
SetsRequiredMembersAttribute
Todos os construtores num tipo com membros obrigatórios, ou cujo tipo base especifica membros obrigatórios, devem ter esses membros definidos por um utilizador quando esse construtor é chamado. A fim de isentar os construtores deste requisito, um construtor pode ser atribuído com SetsRequiredMembersAttribute
, que elimina esses requisitos. O corpo do construtor não é validado para garantir que ele define definitivamente os membros necessários do tipo.
SetsRequiredMembersAttribute
remove todos os requisitos de de um construtor, e esses requisitos não são verificados quanto à validade de forma alguma. NB: esta é a saída de emergência se for necessário herdar de um tipo com uma lista de membros obrigatórios inválida: marque o construtor desse tipo com SetsRequiredMembersAttribute
, e nenhum erro será relatado.
Se um construtor C
se encadear a um construtor base
ou this
que está associado com SetsRequiredMembersAttribute
, C
também deve ser associado com SetsRequiredMembersAttribute
.
Para tipos de registro, emitiremos SetsRequiredMembersAttribute
no construtor de cópia sintetizada de um registro se o tipo de registro ou qualquer um de seus tipos base tiver membros necessários.
NB: Uma versão anterior desta proposta tinha uma metalinguagem maior em torno da inicialização, permitindo adicionar e remover membros individuais necessários de um construtor, bem como a validação de que o construtor estava definindo todos os membros necessários. Isso foi considerado muito complexo para a versão inicial, e removido. Podemos considerar a adição de contratos e modificações mais complexos como um recurso posterior.
Execução
Para cada construtor Ci
no tipo T
com membros necessários R
, os consumidores que invocam Ci
devem optar por uma das seguintes ações:
- Defina todos os membros de
R
em object_initializer na object_creation_expression, - Ou defina todos os membros da
R
através da seção named_argument_list de um attribute_target.
a menos que Ci
seja atribuído a SetsRequiredMembers
.
Se o contexto atual não permite um inicializador de objeto ou não é um alvo de atributo , e Ci
não é atribuído a SetsRequiredMembers
, então é um erro chamar Ci
.
new()
restrição
Um tipo com um construtor sem parâmetros que promove um contrato não pode ser substituído por um parâmetro de tipo restrito a new()
, pois não há maneira de a instanciação genérica garantir que os requisitos sejam cumpridos.
struct
default
s
Os membros necessários não são impostos em instâncias de tipos de struct
criados com default
ou default(StructType)
. Eles são aplicados para instâncias struct
criadas com new StructType()
, mesmo quando StructType
não tem construtor sem parâmetros e o construtor padrão para struct é usado.
Acessibilidade
É um erro marcar um membro obrigatório se o membro não puder ser definido em qualquer contexto onde o tipo de contenção é visível.
- Se o membro for um campo, não pode ser
readonly
. - Se o membro for uma propriedade, ele deve ter um setter ou initer pelo menos tão acessível quanto o tipo de contenção do membro.
Isto significa que não são permitidos os seguintes casos:
interface I
{
int Prop1 { get; }
}
public class Base
{
public virtual int Prop2 { get; set; }
protected required int _field; // Error: _field is not at least as visible as Base. Open question below about the protected constructor scenario
public required readonly int _field2; // Error: required fields cannot be readonly
protected Base() { }
protected class Inner
{
protected required int PropInner { get; set; } // Error: PropInner cannot be set inside Base or Derived
}
}
public class Derived : Base, I
{
required int I.Prop1 { get; } // Error: explicit interface implementions cannot be required as they cannot be set in an object initializer
public required override int Prop2 { get; set; } // Error: this property is hidden by Derived.Prop2 and cannot be set in an object initializer
public new int Prop2 { get; }
public required int Prop3 { get; } // Error: Required member must have a setter or initer
public required int Prop4 { get; internal set; } // Error: Required member setter must be at least as visible as the constructor of Derived
}
É um erro ocultar um membro required
, uma vez que esse membro já não pode ser definido por um consumidor.
Ao substituir um membro required
, deve incluir-se a chave required
na assinatura do método. Isso é feito para que, se algum dia quisermos permitir uma propriedade desnecessária com uma substituição no futuro, tenhamos espaço de design para fazê-lo.
As substituições são permitidas para marcar um membro required
onde ele não é required
no tipo base. Um membro assim marcado é adicionado à lista de membros obrigatórios do tipo derivado.
Os tipos podem substituir as propriedades virtuais necessárias. Isso significa que, se a propriedade virtual base tiver armazenamento e o tipo derivado tentar acessar a implementação base dessa propriedade, eles poderão observar o armazenamento não inicializado. NB: Este é um anti-padrão geral do C#, e não achamos que esta proposta deva tentar resolvê-lo.
Efeito na análise de nulidade
Membros marcados required
não precisam ser inicializados para um estado nulo válido no final de um construtor. Todos os membros required
desse tipo e quaisquer tipos base são considerados pela análise de nulidade como padrão no início de qualquer construtor nesse tipo, a menos que haja encadeamento para um construtor this
ou base
que tenha a atribuição SetsRequiredMembersAttribute
.
A análise de nulidade alertará sobre todos os membros required
dos tipos corrente e base que não possuam um estado de nulidade válido no final de um construtor atribuído com SetsRequiredMembersAttribute
.
#nullable enable
public class Base
{
public required string Prop1 { get; set; }
public Base() {}
[SetsRequiredMembers]
public Base(int unused) { Prop1 = ""; }
}
public class Derived : Base
{
public required string Prop2 { get; set; }
[SetsRequiredMembers]
public Derived() : base()
{
} // Warning: Prop1 and Prop2 are possibly null.
[SetsRequiredMembers]
public Derived(int unused) : base()
{
Prop1.ToString(); // Warning: possibly null dereference
Prop2.ToString(); // Warning: possibly null dereference
}
[SetsRequiredMembers]
public Derived(int unused, int unused2) : this()
{
Prop1.ToString(); // Ok
Prop2.ToString(); // Ok
}
[SetsRequiredMembers]
public Derived(int unused1, int unused2, int unused3) : base(unused1)
{
Prop1.ToString(); // Ok
Prop2.ToString(); // Warning: possibly null dereference
}
}
Representação de metadados
Os 2 atributos a seguir são conhecidos pelo compilador C# e necessários para que esse recurso funcione:
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public sealed class RequiredMemberAttribute : Attribute
{
public RequiredMemberAttribute() {}
}
}
namespace System.Diagnostics.CodeAnalysis
{
[AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)]
public sealed class SetsRequiredMembersAttribute : Attribute
{
public SetsRequiredMembersAttribute() {}
}
}
É um erro aplicar manualmente RequiredMemberAttribute
a um tipo.
Qualquer membro marcado como required
tem um RequiredMemberAttribute
aplicado a ele. Além disso, qualquer tipo que defina tais membros é marcado com RequiredMemberAttribute
, como um marcador para indicar que há membros obrigatórios neste tipo. Observe que, se o tipo B
derivar de A
e A
definir os membros required
, mas B
não adicionar novos nem substituir nenhum dos membros required
existentes, B
não será marcado com um RequiredMemberAttribute
.
Para determinar completamente se há algum membro necessário em B
, é necessário verificar a hierarquia de herança completa.
Qualquer construtor num tipo com membros required
que não tenha SetsRequiredMembersAttribute
aplicado a ele é assinalado com dois atributos:
-
System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute
com o recurso denominado"RequiredMembers"
. -
System.ObsoleteAttribute
com a cadeia de caracteres"Types with required members are not supported in this version of your compiler"
, e o atributo é marcado como um erro, para impedir que compiladores mais antigos usem esses construtores.
Não usamos um modreq
aqui porque é um objetivo manter a compatibilidade binária: se a última propriedade required
fosse removida de um tipo, o compilador já não geraria esta modreq
, o que é uma alteração que quebra a compatibilidade binária, e todos os consumidores precisariam ser recompilados. Um compilador que compreende membros do required
ignorará este atributo obsoleto. Observe que os membros também podem vir de tipos base: mesmo que não haja novos membros required
no tipo atual, se qualquer tipo base tiver required
membros, esse atributo Obsolete
será gerado. Se o construtor já tiver um atributo Obsolete
, nenhum atributo Obsolete
adicional será gerado.
Usamos ObsoleteAttribute
e CompilerFeatureRequiredAttribute
porque este último é novo nesta versão, e os compiladores mais antigos não o entendem. No futuro, podemos ser capazes de abandonar o ObsoleteAttribute
e/ou não usá-lo para proteger novos recursos, mas por enquanto precisamos de ambos para proteção total.
Para criar a lista completa de membros required
R
para um determinado tipo T
, incluindo todos os tipos de base, executa-se o seguinte algoritmo:
- Para cada
Tb
, começando comT
e trabalhando através da cadeia de tipo base até queobject
seja alcançado. - Se
Tb
estiver marcado comRequiredMemberAttribute
, todos os membros doTb
marcados comRequiredMemberAttribute
serão reunidos emRb
- Para cada
Ri
emRb
, seRi
for substituído por qualquer membro daR
, ele será ignorado. - Caso contrário, se qualquer
Ri
for ocultado por um membro doR
, falhará a pesquisa dos membros necessários e nenhuma outra ação será executada. Qualquer chamada a um construtor deT
não associado aSetsRequiredMembers
gera um erro. - Caso contrário,
Ri
será adicionado aR
.
- Para cada
Perguntas abertas
Inicializadores de membros aninhados
Quais serão os mecanismos de aplicação para inicializadores de membros aninhados? Serão totalmente proibidos?
class Range
{
public required Location Start { get; init; }
public required Location End { get; init; }
}
class Location
{
public required int Column { get; init; }
public required int Line { get; init; }
}
_ = new Range { Start = { Column = 0, Line = 0 }, End = { Column = 1, Line = 0 } } // Would this be allowed if Location is a struct type?
_ = new Range { Start = new Location { Column = 0, Line = 0 }, End = new Location { Column = 1, Line = 0 } } // Or would this form be necessary instead?
Questões discutidas
Nível de execução das cláusulas init
O recurso de cláusula init
não foi implementado no C# 11. Continua a ser uma proposta ativa.
Imponhamos estritamente que os membros especificados numa cláusula init
sem inicializador têm de inicializar todos os membros? Parece provável que sim, caso contrário criamos um poço de fracasso fácil. No entanto, também corremos o risco de reintroduzir os mesmos problemas que resolvemos com MemberNotNull
em C# 9. Se quisermos aplicar estritamente isso, provavelmente precisaremos de uma maneira de um método auxiliar para indicar que ele define um membro. Algumas possíveis sintaxes que discutimos para isso:
- Permitir
init
métodos. Esses métodos só podem ser chamados de um construtor ou de outro métodoinit
e podem aceder athis
como se estivesse no construtor (ou seja, campos/propriedadesreadonly
einit
). Isto pode ser combinado com cláusulasinit
destes métodos. Uma cláusulainit
seria considerada como satisfeita se o membro incluído na cláusula fosse definitivamente atribuído no corpo do método ou construtor. Chamar um método com uma cláusulainit
que inclui um membro conta como atribuição a esse membro. Se decidirmos que este é um caminho que queremos seguir, agora ou no futuro, parece provável que não devamos usarinit
como palavra-chave para a cláusula init em um construtor, pois isso seria confuso. - Permita que o operador
!
suprima o aviso/erro explicitamente. Se inicializar um membro de uma maneira complicada (como em um método compartilhado), o usuário pode adicionar um!
à cláusula init para indicar que o compilador não deve verificar a inicialização.
Conclusão: Após discussão, gostamos da ideia do operador !
. Ele permite que o utilizador seja intencional em relação a cenários mais complicados, ao mesmo tempo que não cria um grande buraco de design em torno de métodos init nem precisa anotar todos os métodos como configuradores de membros X ou Y. O !
foi escolhido porque já o usamos para suprimir avisos de nulidade, e utilizá-lo para dizer ao compilador "sou mais esperto que você" noutro contexto é uma extensão natural da forma de sintaxe.
Membros da interface necessários
A presente proposta não permite que as interfaces marquem os membros conforme necessário. Isso nos protege de ter que descobrir cenários complexos em torno de new()
e restrições de interface em genéricos agora, estando diretamente relacionado às fábricas e à construção genérica. A fim de garantir que temos espaço de design nesta área, proibimos required
em interfaces, e proibimos que tipos com required_member_lists sejam substituídos por parâmetros de tipo restritos a new()
. Quando queremos dar uma olhada mais ampla em cenários genéricos de construção com fábricas, podemos revisitar essa questão.
Perguntas sobre sintaxe
O recurso de cláusula init
não foi implementado no C# 11. Continua a ser uma proposta ativa.
- É
init
a palavra certa?init
como modificador de sufixo no construtor pode interferir se quisermos reutilizá-lo para fábricas e também ativar métodosinit
com um modificador de prefixo. Outras possibilidades:set
- É
required
o modificador certo para especificar que todos os membros são inicializados? Outros sugeriram:default
all
- Com um ! para indicar lógica complexa
- Devemos exigir um separador entre o
base
/this
e oinit
?-
:
separador - ', separador
-
- O
required
é o modificador certo? Outras alternativas que foram sugeridas:req
require
mustinit
must
explicit
Conclusão: Removemos a cláusula do construtor init
por enquanto, e estamos prosseguindo com required
como modificador de propriedade.
Restrições da cláusula de inicialização
O recurso de cláusula init
não foi implementado no C# 11. Continua a ser uma proposta ativa.
Devemos permitir o acesso a this
na cláusula init? Se quisermos que a atribuição em init
seja uma abreviação para atribuir o membro no próprio construtor, parece que deveríamos.
Além disso, ele cria um novo escopo, como base()
faz, ou compartilha o mesmo escopo que o corpo do método? Isso é particularmente importante para coisas como funções locais, que a cláusula init pode querer aceder, ou para sombreamento de nomes, se uma expressão init introduz uma variável através do parâmetro out
.
Conclusão: init
cláusula foi suprimida.
Requisitos de acessibilidade e init
O recurso de cláusula init
não foi implementado no C# 11. Continua a ser uma proposta ativa.
Em versões desta proposta com a cláusula init
, falamos sobre poder ter o seguinte cenário:
public class Base
{
protected required int _field;
protected Base() {} // Contract required that _field is set
}
public class Derived : Base
{
public Derived() : init(_field = 1) // Contract is fulfilled and _field is removed from the required members list
{
}
}
No entanto, retirámos a cláusula init
da proposta neste momento, pelo que temos de decidir se permitimos este cenário de forma limitada. As opções que temos são:
- Não permitir o cenário. Esta é a abordagem mais conservadora, e as regras do Accessibility estão atualmente escritas com este pressuposto em mente. A regra é que qualquer membro que seja necessário deve ser pelo menos tão visível quanto o tipo que o contém.
- Exija que todos os construtores sejam:
- Não mais visível do que o membro requerido menos visível.
- Ter o
SetsRequiredMembersAttribute
aplicado ao construtor. Isso garantiria que qualquer pessoa que possa ver um construtor consiga definir tudo o que ele exporta, ou então não há nada para definir. Isso pode ser útil para tipos que só são criados por meio de métodos deCreate
estáticos ou construtores semelhantes, mas a utilidade parece limitada em geral.
- Reintroduza um método para remover partes específicas do contrato na proposta, conforme discutido anteriormente em LDM.
Conclusão: Opção 1, todos os membros necessários devem ser pelo menos tão visíveis quanto o tipo que os contém.
Sobrepor regras
A especificação atual diz que a palavra-chave required
precisa ser copiada e que as substituições podem tornar um membro mais necessário, mas não menos. É isso que queremos fazer?
Permitir a remoção de requisitos requer mais capacidade de modificação de contrato do que estamos propondo atualmente.
Conclusão: É permitido adicionar required
na substituição. Se o membro substituído for required
, o membro substituinte deve ser required
.
Representação alternativa de metadados
Também poderíamos adotar uma abordagem diferente para a representação de metadados, inspirando-se nos métodos de extensão. Poderíamos colocar um RequiredMemberAttribute
no tipo para indicar que o tipo contém membros necessários e, em seguida, colocar um RequiredMemberAttribute
em cada membro que é necessário. Isso simplificaria a sequência de pesquisa (não há necessidade de fazer pesquisa de membros, basta procurar membros com o atributo).
Conclusão: Alternativa aprovada.
Representação de metadados
O Metadata Representation precisa ser aprovado. Além disso, precisamos decidir se esses atributos devem ser incluídos na BCL.
- Para
RequiredMemberAttribute
, este atributo é mais semelhante aos atributos incorporados gerais que usamos para nomes de membros anuláveis/nint/tupla e não é aplicado manualmente pelo utilizador em C#. No entanto, é possível que outros idiomas queiram aplicar manualmente esse atributo. -
SetsRequiredMembersAttribute
, por outro lado, é diretamente utilizado pelos consumidores e, portanto, provavelmente deveria estar na BCL.
Se formos com a representação alternativa na seção anterior, isso pode mudar o cálculo em RequiredMemberAttribute
: em vez de ser semelhante aos atributos incorporados gerais para nomes de membros nint
/nullable/tuple, está mais próximo de System.Runtime.CompilerServices.ExtensionAttribute
, que está na estrutura desde que os métodos de extensão foram enviados.
Conclusão: Vamos colocar ambos os atributos na BCL.
Aviso vs Erro
Não definir um membro obrigatório deve ser considerado um aviso ou um erro? É certamente possível enganar o sistema, via Activator.CreateInstance(typeof(C))
ou similar, o que significa que podemos não ser capazes de garantir totalmente que todas as propriedades estão sempre definidas. Também permitimos a supressão dos diagnósticos no local do construtor usando !
, o que geralmente não permitimos para erros. No entanto, o recurso é semelhante a campos de somente leitura ou propriedades de inicialização, no sentido de que ocorre um erro grave se os utilizadores tentarem definir tal membro após a inicialização. No entanto, podem ser contornados através de reflexão.
Conclusão: Erros.
C# feature specifications