Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Edição campeã: https://github.com/dotnet/csharplang/issues/9499
Resumo
Permitir que uma classe seja declarada closed. Isto impede que classes derivadas diretamente sejam declaradas numa assembleia diferente:
// Assembly 1
public closed record class GateState;
public record class Closed : GateState;
public record class Open(float Percent) : GateState;
// Assembly 2
public record class Locked : GateState; // ERROR - 'GateState' is a closed class
Como todas as classes derivadas são declaradas na assembly da classe fechada, uma expressão consumidora switch que cubra todas elas pode ser concluída para "esgotar" a classe fechada – não é necessário fornecer um caso por defeito para evitar avisos.
// Assembly 3
GateState state = ...;
string description = state switch
{
Closed => "closed",
Open(var percent) => $"{percent}% open"
// No warning about missing cases
};
Motivação
Muitos tipos de classes não são destinados a ser estendidos por ninguém além dos seus autores, mas a linguagem não oferece forma de expressar essa intenção, quanto mais de proteger contra o seu acontecimento. Para os consumidores da classe, isto significa que nenhum conjunto de classes derivadas será considerado como "esgotando" a classe base, e uma expressão switch precisa de incluir um caso genérico para evitar avisos.
As classes fechadas fornecem uma forma de indicar que um conjunto de classes derivadas está completo e permitem que o código consumidor dependa disso para exaustividade nas expressões dos switches.
Design Detalhado
Sintaxe
Permitir closed como modificador nas classes. Uma closed classe é implicitamente abstrata. Assim, também não pode ter um sealed modificador de ou static .
É um erro usar explicitamente um abstract modificador numa closed classe.
Uma classe derivada de uma classe fechada não é ela própria fechada a menos que seja explicitamente declarada como tal.
Restrição do mesmo conjunto
Se uma classe numa assembleia for declarada closed , então é um erro derivar diretamente dela noutra assembleia:
// Assembly 1
public closed class CC { ... }
public class CO : CC { ... } // Ok, same assembly
// Assembly 2
public class C1 : CC { ... } // Error, 'CC' is closed and in a different assembly
public class C2 : CO { ... } // Ok, 'CO' is not closed
A mesma restrição aplica-se aos módulos. Um subtipo de um closed tipo deve estar localizado dentro do mesmo módulo que o tipo base.
Restrição de parâmetros de tipo
Se uma classe genérica deriva diretamente de uma classe fechada, então todos os seus parâmetros de tipo devem ser usados na especificação da classe base:
closed class C<T> { ... }
class D1<U> : C<U> { ... } // Ok, 'U' is used in base class
class D2<V> : C<V[]> { ... } // Ok, 'V' is used in base class
class D3<W> : C<int> { ... } // Error, 'W' is not used in base class
Esta regra serve para garantir que existe uma única instância genérica do tipo derivado que "esgote" uma dada instância genérica do tipo base fechada.
Nota: Esta regra pode não ser suficiente se permitirmos interfaces fechadas em algum momento, porque a) as classes podem implementar múltiplas instâncias genéricas da mesma interface, e b) os parâmetros do tipo de interface podem ser co- ou contravariantes. Nesse ponto, teríamos de refinar a regra para continuar a garantir que só existe uma instância genérica de um dado tipo derivado por instância genérica de um tipo base fechado.
Exaustividade em interruptores
Uma switch expressão que trate de todos os descendentes diretos de uma classe fechada será considerada como tendo esgotado essa classe. Isto significa que alguns avisos de não exaustividade deixarão de ser dados:
CC cc = ...;
_ = cc switch
{
CO co => ...,
// No warning about non-exhaustive switch
};
Por outro lado, isto também significa que pode ser um erro para a classe base fechada ocorrer como um caso após todos os seus descendentes diretos:
_ = cc switch
{
CO co => ...,
CC cc => ..., // Error, case cannot be reached
};
Nota: Podem não existir classes derivadas válidas para certas instâncias genéricas de uma classe base fechada. Um switch exaustivo só precisa de especificar casos para tipos derivados que são realmente possíveis.
Por exemplo:
closed class C<T> { ... }
class D1<U> : C<U> { ... }
class D2<V> : C<V[]> { ... }
Para C<string>, por exemplo, não existe uma instância correspondente de D2<...>, e nenhum caso para D2<...> precisa de ser dado num comutador:
C<string> cs = ...;
_ = cs switch
{
D1<string> d1 => ...,
// No need for a 'D2<...>' case - no instantiation corresponds to 'C<string>'
}
Exaustividade quando um subtipo não pode ser usado
Se um subtipo não for válido num determinado local de uso, devido a violações de restrições, acessibilidade ou outras razões, então não é possível esgotar o switch através dos subtipos.
closed class C;
class D1 : C;
class Container
{
protected class D2 : C;
}
class Program
{
int M(C c)
=> c switch
{
D1 => 1,
// warning: switch is non-exhaustive. Pattern 'C' is not handled.
};
}
Isto também se aplica quando um subtipo genérico não é falável, e a sua aplicabilidade pode depender da substituição final do argumento do tipo.
closed class C<T> { ... }
class D1<U> : C<U> { ... }
class D2<V> : C<V[]> { ... }
class Program
{
int M<X>(C<X> c)
=> c switch
{
D1<X> => 1,
// warning: switch is non-exhaustive. Pattern 'C' is not handled.
};
}
As restrições de subtipo não afetam a exaustividade
A linguagem não refina a determinação de se um subtipo é possível com base em restrições sobre parâmetros de tipo na definição de tipo base e subtipo.
closed class C<T>;
class D1<U1> : C<U1>;
class D2<U2> : C<U2> where U2 : struct;
class Program
{
int M1<X>(C<X> c) where X : class
{
// warning: switch is not exhaustive. Pattern 'C<X>' is not handled.
return c switch
{
D1<X> => 1,
};
}
int M2<X>(C<X> c) where X : class
{
return c switch
{
D1<X> => 1,
C<X> => 2, // ok
};
}
}
Por exemplo, as expressões de troca acima não analisam a construção D2<X>com precisão suficiente, para perceber que todas as possíveis X violações de restrições de U2. Portanto, assume que algum D2<X> é possível e pede ao utilizador que o trate esgotando o tipo base.
Exaustividade quando não existem subtipos
Quando uma classe fechada não tem subtipos, uma troca vazia sobre ela não é considerada exaustiva.
Observações: Isto é assumido como um "estado intermédio" no código normal. O autor provavelmente fará uma alteração para declarar um subtipo neste cenário. Este comportamento equivale a uma "peculiaridade" — apesar de "todos os subtipos 0 serem tratados", a linguagem ainda pede ao utilizador para tratar do tipo base.
closed class C;
class Program
{
int M1(C c)
// warning: switch is not exhaustive.
=> c switch
{
};
int M2(C c)
=> c switch
{
C => 1, // ok
};
}
Exaustividade dos parâmetros de tipo restritos ao tipo fechado
Um parâmetro de tipo restrito a uma classe fechada é tratado de forma semelhante como uma classe fechada para efeitos de verificação de exaustividade.
closed class C;
class D1 : C;
class D2 : C;
class Program
{
int M1<X>(X x) where X : C
=> x switch
{
D1 => 1,
D2 => 2,
};
int M2<X>(X x) where X : C
=> x switch
{
D1 => 1,
D2 => 2,
C => 3, // error: 'C' is subsumed by the previous cases
};
}
Determinação de subtipos de uma classe fechada
A exaustividade das comutações sobre tipos de classe fechada é determinada verificando se a comutação é exaustiva sobre o conjunto de subtipos do tipo de classe fechada de entrada.
O conjunto de subtipos S de uma classe fechada é determinado da seguinte forma:
- Para um dado tipo
Cfechado , sejaC₀a sua definição original. - Para cada declaração
S₀de subtipo cujo tipo base tenha definiçãoC₀original , determine se existe uma construçãoSque tenha tipoCbase .- Ver também §19.6.3 Unicidade das interfaces implementadas na norma.
- Se tal
Sexistir, está incluído no conjunto de subtipos.
Convertibilidade de interface de classes fechadas
Diz-se que uma classe fechada tem uma hierarquia selada, se todos os seus subtipos estiverem selados ou tiverem uma hierarquia selada. Ou seja, todas as classes na hierarquia expandida estão ou seladas ou fechadas.
Quando uma classe fechada tem uma hierarquia selada, então é introduzida uma restrição de conversibilidade na interface . Isto impede uma tentativa de conversão para tipo de interface, que nunca poderia ter sucesso.
Esta restrição é semelhante em natureza à conversão explícita de referência de um tipo de classe selada para um tipo de interface. Ver §10.3.5 Conversões de referências explícitas.
var c = new C();
var i = (I)c; // error
closed class C { }
sealed class D1 : C { }
sealed class D2 : C { }
interface I { }
Determinamos se a conversão explícita de referência de C para I existe, reunindo recursivamente o conjunto de interfaces implementadas por C e os seus subtipos. Se o conjunto de interfaces inclui I, e C não implementa I, então a conversão explícita de referência existe de C para I. (No caso de implementar CI, então está disponível uma conversão implícita de referência.)
Rebaixamento
As classes fechadas são geradas com um IsClosedType atributo, para permitir que sejam reconhecidas por um compilador consumidor.
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class IsClosedTypeAttribute : Attribute { }
}
Bloqueio de subtipagem de outras linguagens/compiladores
As classes fechadas não devem ser herdadas de linguagens que não suportam classes fechadas. Isto é conseguido somando [CompilerFeatureRequired("ClosedClasses")] a todos os construtores de classes fechadas.
// Authoring assembly, built with .NET 10 SDK
closed class C1
{
public C1() { }
public C1(int param) { }
}
// Consuming assembly, built with .NET 8 SDK
class C2 : C1
{
public C2() { } // error: 'C1.C1()' requires compiler feature "ClosedClasses"
public C2() : base(42) { } // error: 'C1.C1(int)' requires compiler feature "ClosedClasses"
}
"Vista" de metadados de C1:
[IsClosedType]
class C1
{
[CompilerFeatureRequired("ClosedClasses")]
public C1() { }
[CompilerFeatureRequired("ClosedClasses")]
public C1(int param) { }
}
Note que, ao contrário da funcionalidade "membros obrigatórios", um ObsoleteAttribute não é emitido além do CompilerFeatureRequiredAttribute. Apenas esta última é emitida.
Múltiplos Atributos de FuncionalidadeRequerida do Compilador
Num cenário como o seguinte, o compilador emitirá um , separado CompilerFeatureRequiredpara cada característica necessária que seja relevante para o símbolo:
closed class C1
{
public C() { }
public required string P { get; set; }
}
// Metadata:
class C1
{
[Obsolete("Types with required members are not supported in this version of your compiler")]
[CompilerFeatureRequired("RequiredMembers")]
[CompilerFeatureRequired("ClosedClasses")]
public C1() { }
}
Desvantagens
- Pode ser uma alteração decisiva adicionar um
closedmodificador a uma classe existente, ou adicionar uma classe derivada adicional a partir de uma classe fechada. Antes de publicar uma aula fechada, o autor deve considerar o contrato de longo prazo que implica com os seus consumidores.
Alternatives
- Em vez de um novo
closedmodificador, uma classe fechada podia ser designada com um[Closed]atributo. - O âmbito onde os descendentes são permitidos poderia ser restringido ainda mais a um ficheiro (embora isso não tivesse muitos precedentes em C#) ou ao interior do corpo da classe fechada como classes aninhadas.
- O conjunto fechado dos descendentes permitidos poderia ser dado como uma lista em vez de implícito pelo local onde ocorrem declarações. Isto permitiria a inclusão de classes noutras assembleias.
Funcionalidades opcionais
- As interfaces também podiam ser permitidas para serem fechadas. As regras seriam muito semelhantes.
Perguntas abertas
N/A
C# feature specifications