Gostaria de reunir o máximo de informações possível sobre a versão da API no .NET / CLR e, especificamente, sobre como as alterações na API quebram ou não os aplicativos clientes. Primeiro, vamos definir alguns termos:
Alteração na API - uma alteração na definição publicamente visível de um tipo, incluindo qualquer um de seus membros públicos. Isso inclui alterar o tipo e os nomes dos membros, alterar o tipo base de um tipo, adicionar / remover interfaces da lista de interfaces implementadas de um tipo, adicionar / remover membros (incluindo sobrecargas), alterar a visibilidade do membro, renomear métodos e renomear parâmetros, adicionar valores padrão para parâmetros de método, adicionando / removendo atributos em tipos e membros e adicionando / removendo parâmetros de tipo genérico em tipos e membros (perdi alguma coisa?). Isso não inclui nenhuma alteração nos corpos dos membros ou quaisquer alterações nos membros privados (ou seja, não levamos em consideração a Reflexão).
Quebra no nível binário - uma alteração na API que resulta em assemblies de cliente compilados em relação à versão mais antiga da API, potencialmente não carregando com a nova versão. Exemplo: alterando a assinatura do método, mesmo que permita ser chamado da mesma maneira que antes (ou seja: void para retornar sobrecargas nos valores padrão do tipo / parâmetro).
Quebra no nível da fonte - uma alteração na API que resulta no código existente gravado para compilar em uma versão mais antiga da API, potencialmente não compilando com a nova versão. Entretanto, os assemblies de cliente já compilados funcionam como antes. Exemplo: adicionando uma nova sobrecarga que pode resultar em ambiguidade nas chamadas de método que não eram ambíguas anteriormente.
Mudança de semântica silenciosa no nível de origem - uma mudança de API que resulta em código existente gravado para compilar em uma versão mais antiga da API muda silenciosamente sua semântica, por exemplo, chamando um método diferente. No entanto, o código deve continuar a compilar sem avisos / erros, e os assemblies compilados anteriormente devem funcionar como antes. Exemplo: implementando uma nova interface em uma classe existente que resulta em uma sobrecarga diferente sendo escolhida durante a resolução da sobrecarga.
O objetivo final é catalogar o máximo possível de alterações de API de semântica de interrupção e silêncio e descrever o efeito exato da quebra e quais idiomas são e não são afetados por ela. Para expandir o último: enquanto algumas mudanças afetam todos os idiomas universalmente (por exemplo, adicionar um novo membro a uma interface interrompe as implementações dessa interface em qualquer idioma), algumas requerem semânticas de idiomas muito específicas para entrar em jogo para obter uma pausa. Isso geralmente envolve sobrecarga de método e, em geral, qualquer coisa relacionada a conversões implícitas de tipo. Parece não haver nenhuma maneira de definir o "denominador menos comum" aqui, mesmo para idiomas compatíveis com CLS (ou seja, aqueles que estão em conformidade pelo menos com as regras do "consumidor CLS", conforme definido nas especificações da CLI) - embora eu ' Eu aprecio se alguém me corrigir como errado aqui - então isso terá que ir idioma por idioma. Os de maior interesse são naturalmente os que vêm com o .NET pronto para uso: C #, VB e F #; mas outros, como IronPython, IronRuby, Delphi Prism etc. também são relevantes. Quanto mais difícil for o caso, mais interessante será - coisas como remover membros são bastante evidentes, mas interações sutis entre, por exemplo, sobrecarga de método, parâmetros opcionais / padrão, inferência do tipo lambda e operadores de conversão podem ser muito surpreendentes às vezes.
Alguns exemplos para o kickstart:
Adicionando novas sobrecargas de método
Tipo: quebra no nível da fonte
Idiomas afetados: C #, VB, F #
API antes da alteração:
public class Foo
{
public void Bar(IEnumerable x);
}
API após alteração:
public class Foo
{
public void Bar(IEnumerable x);
public void Bar(ICloneable x);
}
Exemplo de código do cliente trabalhando antes da alteração e quebrado após ela:
new Foo().Bar(new int[0]);
Incluindo novas sobrecargas implícitas do operador de conversão
Tipo: quebra no nível da fonte.
Idiomas afetados: C #, VB
Idiomas não afetados: F #
API antes da alteração:
public class Foo
{
public static implicit operator int ();
}
API após alteração:
public class Foo
{
public static implicit operator int ();
public static implicit operator float ();
}
Exemplo de código do cliente trabalhando antes da alteração e quebrado após ela:
void Bar(int x);
void Bar(float x);
Bar(new Foo());
Notas: F # não está quebrado, porque ele não tem qualquer apoio nível de linguagem para operadores sobrecarregados, nem explícitas nem implícitas - ambos tem que ser chamado diretamente como op_Explicit
e op_Implicit
métodos.
Adicionando novos métodos de instância
Tipo: a semântica silenciosa no nível da fonte é alterada.
Idiomas afetados: C #, VB
Idiomas não afetados: F #
API antes da alteração:
public class Foo
{
}
API após alteração:
public class Foo
{
public void Bar();
}
Código de cliente de amostra que sofre uma alteração semântica silenciosa:
public static class FooExtensions
{
public void Bar(this Foo foo);
}
new Foo().Bar();
Notas: O F # não está quebrado, porque não possui suporte para o nível de idioma ExtensionMethodAttribute
e requer que os métodos de extensão CLS sejam chamados como métodos estáticos.
fonte
Respostas:
Alterando uma assinatura de método
Tipo: Ruptura de nível binário
Idiomas afetados: C # (VB e F # provavelmente, mas não testado)
API antes da alteração
API após alteração
Código de cliente de amostra funcionando antes da alteração
fonte
bar
.Adicionando um parâmetro com um valor padrão.
Tipo de quebra: quebra de nível binário
Mesmo que o código-fonte de chamada não precise ser alterado, ele ainda precisará ser recompilado (como ao adicionar um parâmetro regular).
Isso ocorre porque o C # compila os valores padrão dos parâmetros diretamente no assembly de chamada. Isso significa que, se você não recompilar, receberá uma MissingMethodException porque o assembly antigo tenta chamar um método com menos argumentos.
API antes da alteração
API após alteração
Código de cliente de amostra que é quebrado posteriormente
O código do cliente precisa ser recompilado no
Foo(5, null)
nível do bytecode. A montagem chamada conterá apenasFoo(int, string)
, nãoFoo(int)
. Isso ocorre porque os valores padrão dos parâmetros são puramente um recurso de linguagem, o tempo de execução .Net não sabe nada sobre eles. (Isso também explica por que os valores padrão precisam ser constantes em tempo de compilação em C #).fonte
Func<int> f = Foo;
// isto irá falhar com a assinatura mudouEste foi muito óbvio quando o descobri, especialmente à luz da diferença com a mesma situação para interfaces. Não é uma pausa, mas é surpreendente o suficiente que eu decidi incluí-la:
Refatorando os Membros da Classe em uma Classe Base
Tipo: não uma pausa!
Idiomas afetados: nenhum (ou seja, nenhum está quebrado)
API antes da alteração:
API após alteração:
Código de exemplo que continua trabalhando durante toda a alteração (mesmo que eu esperasse que fosse interrompida):
Notas:
C ++ / CLI é a única linguagem .NET que possui uma construção análoga à implementação explícita da interface para membros da classe base virtual - "substituição explícita". Eu esperava que isso resultasse no mesmo tipo de quebra que ao mover membros da interface para uma interface base (já que a IL gerada para substituição explícita é a mesma que para implementação explícita). Para minha surpresa, esse não é o caso - embora a IL gerada ainda especifique que
BarOverride
substituições, emFoo::Bar
vez deFooBase::Bar
, o carregador de montagem é inteligente o suficiente para substituir uma pela outra corretamente, sem queixas - aparentemente, o fato de queFoo
é uma classe é o que faz a diferença. Vai saber...fonte
Este é um caso especial talvez não tão óbvio de "adicionar / remover membros da interface", e achei que ele merece sua própria entrada à luz de outro caso que vou publicar a seguir. Assim:
Refatorando Membros da Interface em uma Interface Base
Tipo: quebras nos níveis de origem e binário
Idiomas afetados: C #, VB, C ++ / CLI, F # (para quebra de origem; o binário afeta naturalmente qualquer idioma)
API antes da alteração:
API após alteração:
Código de cliente de amostra que está quebrado por alterações no nível de origem:
Código de cliente de amostra que é quebrado por alterações no nível binário;
Notas:
Para uma quebra no nível da fonte, o problema é que C #, VB e C ++ / CLI exigem o nome exato da interface na declaração de implementação dos membros da interface; portanto, se o membro for movido para uma interface base, o código não será mais compilado.
A quebra binária se deve ao fato de os métodos de interface serem totalmente qualificados na IL gerada para implementações explícitas, e o nome da interface também deve ser exato.
A implementação implícita, quando disponível (por exemplo, C # e C ++ / CLI, mas não o VB) funcionará bem no nível de origem e binário. As chamadas de método também não são interrompidas.
fonte
Implements IFoo.Bar
fará referência transparenteIFooBase.Bar
?Reordenando valores enumerados
Tipo de interrupção: semântica silenciosa no nível de origem / nível binário
Idiomas afetados: todos
Reordenar valores enumerados manterá a compatibilidade no nível da fonte, pois os literais têm o mesmo nome, mas seus índices ordinais serão atualizados, o que pode causar alguns tipos de interrupções silenciosas no nível da fonte.
Pior ainda são as quebras de nível binário silenciosas que podem ser introduzidas se o código do cliente não for recompilado na nova versão da API. Os valores enum são constantes em tempo de compilação e, como tal, qualquer uso deles é inserido na IL do assembly do cliente. Às vezes, esse caso pode ser particularmente difícil de detectar.
API antes da alteração
API após alteração
Exemplo de código do cliente que funciona, mas é quebrado posteriormente:
fonte
Essa é realmente uma coisa muito rara na prática, mas, mesmo assim, surpreendente quando isso acontece.
Adicionando novos membros não sobrecarregados
Tipo: quebra no nível da fonte ou mudança semântica silenciosa.
Idiomas afetados: C #, VB
Idiomas não afetados: F #, C ++ / CLI
API antes da alteração:
API após alteração:
Código de cliente de amostra quebrado por alteração:
Notas:
O problema aqui é causado pela inferência do tipo lambda em C # e VB na presença de resolução de sobrecarga. Uma forma limitada de tipagem de pato é empregada aqui para romper laços onde mais de um tipo corresponde, verificando se o corpo do lambda faz sentido para um determinado tipo - se apenas um tipo resultar em corpo compilável, esse será escolhido.
O perigo aqui é que o código do cliente pode ter um grupo de métodos sobrecarregado, em que alguns métodos usam argumentos de seus próprios tipos e outros usam argumentos de tipos expostos por sua biblioteca. Se algum código dele se basear no algoritmo de inferência de tipo para determinar o método correto com base apenas na presença ou ausência de membros, a adição de um novo membro a um de seus tipos com o mesmo nome que em um dos tipos do cliente pode gerar inferência desativado, resultando em ambiguidade durante a resolução de sobrecarga.
Observe que os tipos
Foo
eBar
neste exemplo não estão relacionados de forma alguma, nem por herança nem por outro motivo. O simples uso deles em um único grupo de métodos é suficiente para acionar isso e, se isso ocorrer no código do cliente, você não terá controle sobre ele.O código de exemplo acima demonstra uma situação mais simples em que essa é uma quebra no nível da fonte (ou seja, resultados de erro do compilador). No entanto, isso também pode ser uma mudança semântica silenciosa, se a sobrecarga escolhida por inferência tiver outros argumentos que, de outra forma, a classificariam abaixo (por exemplo, argumentos opcionais com valores padrão ou incompatibilidade de tipo entre o argumento declarado e o real que requer um implícito) conversão). Nesse cenário, a resolução de sobrecarga não falhará mais, mas uma sobrecarga diferente será silenciosamente selecionada pelo compilador. Na prática, no entanto, é muito difícil encontrar esse caso sem construir cuidadosamente assinaturas de método para causá-lo deliberadamente.
fonte
Converta uma implementação implícita da interface em explícita.
Tipo de interrupção: origem e binário
Idiomas afetados: todos
Isso é realmente apenas uma variação da alteração da acessibilidade de um método - é apenas um pouco mais sutil, pois é fácil ignorar o fato de que nem todo acesso aos métodos de uma interface é necessariamente através de uma referência ao tipo da interface.
API antes da alteração:
API após alteração:
Código de cliente de amostra que funciona antes da alteração e é quebrado posteriormente:
fonte
Converta uma implementação explícita da interface em implícita.
Tipo de intervalo: Fonte
Idiomas afetados: todos
A refatoração de uma implementação explícita da interface em uma implícita é mais sutil na maneira como ela pode quebrar uma API. Na superfície, parece que isso deve ser relativamente seguro; no entanto, quando combinado com herança, pode causar problemas.
API antes da alteração:
API após alteração:
Código de cliente de amostra que funciona antes da alteração e é quebrado posteriormente:
fonte
Foo
não havia um método público chamadoGetEnumerator
, e você está chamando o método por meio de uma referência do tipoFoo
.. .yield return "Bar"
:), mas sim, eu vejo aonde isso está indo agora -foreach
sempre chama o método público chamadoGetEnumerator
, mesmo que não seja a implementação realIEnumerable.GetEnumerator
. Isso parece ter mais um ângulo: mesmo que você tenha apenas uma classe, e ela implementeIEnumerable
explicitamente, isso significa que é uma mudança de origem para adicionar um método público nomeadoGetEnumerator
a ela, porque agoraforeach
usará esse método na implementação da interface. Além disso, o mesmo problema é aplicável àIEnumerator
implementação ...Alterando um campo para uma propriedade
Tipo de interrupção: API
Idiomas afetados: Visual Basic e C # *
Informações: Quando você altera um campo ou variável normal em uma propriedade no visual basic, qualquer código externo que faça referência a esse membro de qualquer forma precisará ser recompilado.
API antes da alteração:
API após alteração:
Exemplo de código do cliente que funciona, mas é quebrado posteriormente:
fonte
out
e osref
argumentos dos métodos, ao contrário dos campos, e não podem ser o alvo do&
operador unário .Adição de namespace
Quebra no nível da fonte / semântica silenciosa no nível da fonte
Devido à maneira como a resolução do espaço para nome funciona no vb.Net, adicionar um espaço para nome a uma biblioteca pode fazer com que o código do Visual Basic compilado com uma versão anterior da API não compile com uma nova versão.
Código de cliente de amostra:
Se uma nova versão da API adicionar o espaço para nome
Api.SomeNamespace.Data
, o código acima não será compilado.Torna-se mais complicado com as importações de namespace no nível do projeto. Se
Imports System
for omitido do código acima, mas oSystem
espaço para nome for importado no nível do projeto, o código ainda poderá resultar em um erro.No entanto, se a API incluir uma classe
DataRow
em seuApi.SomeNamespace.Data
espaço para nome, o código será compilado, masdr
será uma instância deSystem.Data.DataRow
quando compilado com a versão antiga da API eApi.SomeNamespace.Data.DataRow
quando compilado com a nova versão da API.Renomeação de Argumento
Interrupção no nível da fonte
Alterar os nomes dos argumentos é uma alteração de quebra no vb.net da versão 7 (?) (.Net versão 1?) E c # .net da versão 4 (.Net versão 4).
API antes da alteração:
API após alteração:
Código de cliente de amostra:
Parâmetros de referência
Interrupção no nível da fonte
A adição de uma substituição de método com a mesma assinatura, exceto que um parâmetro é passado por referência e não por valor, fará com que a origem vb que referencia a API não consiga resolver a função. O Visual Basic não tem como (?) Diferenciar esses métodos no ponto de chamada, a menos que tenham nomes de argumentos diferentes, portanto, essa alteração pode fazer com que ambos os membros sejam inutilizáveis do código vb.
API antes da alteração:
API após alteração:
Código de cliente de amostra:
Alteração de campo para propriedade
Interrupção no nível binário / Interrupção no nível da fonte
Além da quebra no nível binário óbvia, isso pode causar uma quebra no nível da fonte se o membro for passado para um método por referência.
API antes da alteração:
API após alteração:
Código de cliente de amostra:
fonte
Alteração na API:
Quebra de nível binário:
Adicionando um novo membro (protegido por evento) que usa um tipo de outro assembly (Class2) como uma restrição de argumento de modelo.
Alterar uma classe filho (Class3) para derivar de um tipo em outro assembly quando a classe é usada como argumento de modelo para essa classe.
A semântica silenciosa no nível da fonte é alterada:
(não sei onde eles se encaixam)
Alterações na implantação:
Alterações na inicialização / configuração:
Atualizar:
Desculpe, eu não percebi que a única razão pela qual isso estava quebrando para mim era o fato de usá-los em restrições de modelo.
fonte
TypeForwardedToAttribute
for usado.-Werror
o sistema de compilação que você envia com tarballs de lançamento. Esse sinalizador é mais útil para o desenvolvedor do código e geralmente não é útil para o consumidor.Incluindo métodos de sobrecarga para diminuir o uso de parâmetros padrão
Tipo de interrupção: a semântica silenciosa no nível da fonte é alterada
Como o compilador transforma chamadas de método com valores de parâmetros padrão ausentes em uma chamada explícita com o valor padrão no lado da chamada, é dada compatibilidade para o código compilado existente; um método com a assinatura correta será encontrado para todo o código compilado anteriormente.
Por outro lado, as chamadas sem o uso de parâmetros opcionais agora são compiladas como uma chamada para o novo método que está ausente no parâmetro opcional. Tudo ainda está funcionando bem, mas se o código chamado residir em outro assembly, o código compilado recentemente chamando agora será dependente da nova versão deste assembly. A implantação de assemblies que chamam o código refatorado sem também implantar o assembly em que o código refatorado reside está resultando em exceções "método não encontrado".
API antes da alteração
API após alteração
Código de exemplo que ainda estará funcionando
Código de exemplo que agora depende da nova versão ao compilar
fonte
Renomeando uma interface
Meio que Break: Source e Binary
Idiomas afetados: Provavelmente todos, testados em C #.
API antes da alteração:
API após alteração:
Exemplo de código do cliente que funciona, mas é quebrado posteriormente:
fonte
Método de sobrecarga com um parâmetro do tipo anulável
Tipo: quebra no nível da fonte
Idiomas afetados: C #, VB
API antes de uma alteração:
API após a alteração:
Exemplo de código do cliente trabalhando antes da alteração e quebrado após ela:
Exceção: a chamada é ambígua entre os seguintes métodos ou propriedades.
fonte
Promoção para um método de extensão
Tipo: quebra no nível da fonte
Idiomas afetados: C # v6 e superior (talvez outros?)
API antes da alteração:
API após alteração:
Exemplo de código do cliente trabalhando antes da alteração e quebrado após ela:
Mais informações: https://github.com/dotnet/csharplang/issues/665
fonte