Pergunta

Gostaria de reunir o máximo de informações possível sobre API de versões em .NET / CLR e, especificamente, como mudanças na API faz ou não quebrar aplicativos cliente. Primeiro, vamos definir alguns termos:

mudança API - uma mudança na definição publicamente visível de um tipo, incluindo qualquer um dos seus membros públicos. Isto inclui a alteração nomes de tipos e membros, como alterar o tipo de base de um tipo, adicionar / remover as interfaces de lista de interfaces implementadas de um tipo, adicionar / remover membros (incluindo sobrecargas), mudando a visibilidade membro, renomeando método e tipo de parâmetros, adicionando valores padrão para parâmetros de método, adicionar / remover atributos em tipos e membros, e adicionar / remover parâmetros de tipo genérico sobre os tipos e membros (que eu perdi alguma coisa?). Isso não inclui quaisquer alterações em organismos membros, ou quaisquer alterações aos membros privados (ou seja, que não levam em conta Reflexão).

Binary-nível de quebra - uma mudança API que resulta em montagens cliente compilados contra a versão mais antiga do API potencialmente não carregar com a nova versão. Exemplo: mudança de assinatura do método, mesmo que permite a ser chamado da mesma forma como antes (ou seja: void para retornar valores de tipo / parâmetro padrão sobrecargas).

Fonte de nível ruptura - uma mudança API que resulta em código existente escrito para compilar contra versão mais antiga do API potencialmente não compilar com a nova versão. Já compilado montagens cliente funcionar como antes, no entanto. Exemplo:. Adicionando uma nova sobrecarga que pode resultar em ambigüidade em chamadas de método que eram inequívocas anterior

Fonte de nível mudança semântica tranquilos - uma mudança API que resulta em código existente escrito para compilar contra versão mais antiga do API mudar tranquilamente sua semântica, por exemplo, chamando um método diferente. O código deverá, contudo, continuar a compilar sem avisos / erros, e conjuntos compilados anteriormente deve funcionar como antes. Exemplo:. Implementar uma nova interface em uma classe existente que resulta em uma sobrecarga diferente a ser escolhido durante a resolução de sobrecarga

O objetivo final é catalogize como muitos quebra e tranquilos mudanças na API semântica como possível, e descrever efeito exato de quebra, e quais idiomas são e não são afetados por ela. Para expandir a este último: enquanto algumas alterações afetam todas as línguas universalmente (por exemplo, a adição de um novo membro para uma interface vai quebrar implementações de que a interface em qualquer idioma), alguns exigem semântica da linguagem muito específicas para entrar em jogo para obter uma pausa. Este é o mais tipicamente envolve a sobrecarga de método, e, em geral, nada tendo a ver com conversões de tipo implícitas. Não parece haver nenhuma maneira para definir o "mínimo denominador comum" aqui mesmo para CLS-conformant línguas (ou seja, que satisfazem, pelo menos a normativa de "CLS consumidor", conforme definido no CLI especificação) - embora eu vou apreciar se alguém me corrige como sendo errado aqui - de modo que este terá que ir a linguagem pela linguagem. Aqueles de maior interesse são naturalmente os que vêm com .NET fora da caixa: C #, VB e F #; mas outros, como o IronPython, IronRuby, Delphi Prism etc também são relevantes. A mais de um caso de canto que é, o mais interessante será - coisas como a remoção de membros são bastante auto-evidente, mas interações sutis entre por exemplo sobrecarga de método, parâmetros / padrão opcionais, tipo lambda inferência, e de conversão operadores podem ser muito surpreendente, às vezes.

Alguns exemplos para alavancar o seguinte:

Adicionar novo método sobrecargas

Tipo: break-nível de fonte

Idiomas afetados: C #, VB, F #

API antes da mudança:

public class Foo
{
    public void Bar(IEnumerable x);
}

API após a mudança:

public class Foo
{
    public void Bar(IEnumerable x);
    public void Bar(ICloneable x);
}

Amostra de trabalho de código do cliente antes da mudança e quebrado depois que:

new Foo().Bar(new int[0]);

Adicionando novo operador de conversão implícita sobrecargas

Tipo:. Break-nível de fonte

Languages ??umffected: C #, VB

As línguas não afetados: F #

API antes da mudança:

public class Foo
{
    public static implicit operator int ();
}

API após a mudança:

public class Foo
{
    public static implicit operator int ();
    public static implicit operator float ();
}

Amostra de trabalho de código do cliente antes da mudança e quebrado depois que:

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 têm de ser chamado diretamente como métodos op_Explicit e op_Implicit

A adição de novos métodos de instância

Tipo:. De nível de origem semântica tranquilos mudança

Idiomas afetados: C #, VB

As línguas não afetados: F #

API antes da mudança:

public class Foo
{
}

API após a mudança:

public class Foo
{
    public void Bar();
}

código de cliente Sample que sofre mudança numa tranquila semântica:

public static class FooExtensions
{
    public void Bar(this Foo foo);
}

new Foo().Bar();

Notas: F # não está quebrado, porque ele não tem suporte nível de linguagem para ExtensionMethodAttribute, e requer métodos de extensão CLS para ser chamado como métodos estáticos.

Foi útil?

Solução

Alterar uma assinatura do método

Tipo: binário de nível Pausa

Idiomas afetados: C # (VB e F # mais provável, mas não testado)

API antes da mudança

public static class Foo
{
    public static void bar(int i);
}

API após a mudança

public static class Foo
{
    public static bool bar(int i);
}

Amostra de trabalho de código do cliente antes da mudança

Foo.bar(13);

Outras dicas

Adicionando um parâmetro com um valor padrão.

Tipo de Break: nível binário pausa

Mesmo que o código-fonte de chamada não precisa mudar, ele ainda precisa ser recompilados (assim como ao adicionar um parâmetro regular).

Isso é porque C # compila os valores padrão dos parâmetros diretamente para o chamando de montagem. Isso significa que se você não recompilar, você vai ter um MissingMethodException porque as antigas tentativas de montagem para chamar um método com menos argumentos.

API Antes de Mudança

public void Foo(int a) { }

API Depois de Mudança

public void Foo(int a, string b = null) { }

código de cliente de amostra que está quebrado depois

Foo(5);

O código do cliente precisa ser recompilados em Foo(5, null) no nível de bytecode. O chamado montagem conterá somente Foo(int, string), não Foo(int). Isso porque os valores dos parâmetros padrão são puramente um recurso de linguagem, o .NET runtime não sabe nada sobre eles. (Isso também explica por que os valores padrão tem que ser constantes de tempo de compilação em C #).

Este era muito não-óbvia quando descobri-lo, especialmente à luz da diferença com a mesma situação para interfaces. Não é uma pausa em tudo, mas é surpreendente o suficiente para que eu decidi incluí-lo:

refatoração os alunos em uma classe base

Tipo: não uma ruptura

Idiomas afetados: nenhum (ou seja, nenhum deles está quebrado)

API antes da mudança:

class Foo
{
    public virtual void Bar() {}
    public virtual void Baz() {}
}

API após a mudança:

class FooBase
{
    public virtual void Bar() {}
}

class Foo : FooBase
{
    public virtual void Baz() {}
}

O código de exemplo que continua trabalhando em toda a mudança (mesmo que eu esperava para quebrar):

// C++/CLI
ref class Derived : Foo
{
   public virtual void Baz() {{

   // Explicit override    
   public virtual void BarOverride() = Foo::Bar {}
};

Notas:

C ++ / CLI é a única linguagem .NET que tem um análogo construção para implementação de interface explícita para os membros da classe base virtual - "override explícito". Eu totalmente esperado que a resultar no mesmo tipo de ruptura como quando se deslocam os membros de interface para uma interface de base (desde IL gerado por substituição explícito é o mesmo como para a implementação explícita). Para minha surpresa, este não é o caso - embora gerado IL ainda especifica que BarOverride substituições Foo::Bar em vez de FooBase::Bar, montagem loader é inteligente o suficiente para substituir um pelo outro corretamente sem quaisquer queixas - aparentemente, o fato de que Foo é uma classe é o que faz a diferença. Vai entender ...

Este é um caso especial, talvez não tão óbvia de "adicionar / remover membros de interface", e eu percebi que merece a sua própria entrada na luz de um outro caso em que eu vou postar seguinte. Assim:

refatoração membros de interface em uma base de interface

Tipo: breaks, tanto a fonte e os níveis de binários

Idiomas afetados: C #, VB, C ++ / CLI, F # (para quebra de fonte, um binário afeta naturalmente qualquer língua)

API antes da mudança:

interface IFoo
{
    void Bar();
    void Baz();
}

API após a mudança:

interface IFooBase 
{
    void Bar();
}

interface IFoo : IFooBase
{
    void Baz();
}

código de cliente Sample que é quebrado pela mudança no nível de fonte:

class Foo : IFoo
{
   void IFoo.Bar() { ... }
   void IFoo.Baz() { ... }
}

código de cliente Sample que é quebrado pela mudança no nível binário;

(new Foo()).Bar();

Notas:

Para quebra de nível fonte, o problema é que C #, VB e C ++ / CLI todos requerem exata nome da interface na declaração de implementação de membro interface; Assim, se o membro é movido a uma interface de base, o código já não compilar.

Binary ruptura é devido ao fato de que os métodos de interface são totalmente qualificado em IL gerado para implementações explícitas, e nome da interface também deve ser exata.

implementação implícita quando disponíveis (ou seja, C # e C ++ / CLI, mas não VB) vai funcionar bem em ambos fonte e nível binário. chamadas de método não quebrar qualquer um.

Reordenamento enumerou valores

Kind of pausa: Fonte de nível / binário de nível semântica tranquilos mudança

Idiomas afetados: todos

Reordenamento valores enumerados irá manter a compatibilidade de nível de origem como literais têm o mesmo nome, mas seus índices ordinais será atualizado, que pode causar alguns tipos de quebras de nível de fonte silenciosas.

Ainda pior é as quebras de nível binário silenciosas que podem ser introduzidas se o código do cliente não é recompilados contra a nova versão da API. enum valores são as constantes de tempo de compilação e, como tal, quaisquer utilizações de eles são cozidos em conjunto o cliente é IL. Este caso pode ser particularmente difícil de detectar, às vezes.

API Antes de Mudança

public enum Foo
{
   Bar,
   Baz
}

API Depois de Mudança

public enum Foo
{
   Baz,
   Bar
}

código de cliente de exemplo que funciona, mas está quebrado depois:

Foo.Bar < Foo.Baz

Este é realmente uma coisa muito rara na prática, mas mesmo assim uma surpreendente um quando isso acontece.

acrescentar novos membros não-sobrecarregada

Tipo:. Pausa nível de fonte ou semântica tranquilos mudança

Idiomas afetados: C #, VB

As línguas não afetados: F #, C ++ / CLI

API antes da mudança:

public class Foo
{
}

API após a mudança:

public class Foo
{
    public void Frob() {}
}

código de cliente Sample que é quebrado pela mudança:

class Bar
{
    public void Frob() {}
}

class Program
{
    static void Qux(Action<Foo> a)
    {
    }

    static void Qux(Action<Bar> a)
    {
    }

    static void Main()
    {
        Qux(x => x.Frob());        
    }
}

Notas:

O problema aqui é causada por tipo lambda inferência em C # e VB na presença de resolução de sobrecarga. Uma forma limitada de tipagem pato é empregada aqui para quebrar laços, onde mais de um tipo partidas, verificando se o corpo do lambda faz sentido para um determinado tipo -. Se apenas um tipo resulta em corpo compilable, que um é escolhido

O perigo aqui é que o código do cliente pode ter um grupo método sobrecarregado onde alguns métodos levam argumentos dos seus próprios tipos, e outros tomam argumentos dos tipos expostos por sua biblioteca. Se algum de seu código, em seguida, se baseia no algoritmo de inferência de tipos para determinar o método correto baseado unicamente em presença ou ausência de membros, então a adição de um novo membro para um de seus tipos com o mesmo nome em um dos tipos do cliente pode potencialmente lançar inferência off, resultando em ambigüidade durante a resolução de sobrecarga.

Note que os tipos Foo e Bar neste exemplo não estão relacionados de alguma forma, não por herança, nem de outra forma. O mero uso deles em um grupo único método é suficiente para desencadear isso, e se isso ocorrer no código do cliente, você não tem controle sobre ele.

O código de exemplo acima demonstra uma situação mais simples, onde este é um break-nível de fonte (ou seja compilador resultados de erro). No entanto, isso também pode ser uma mudança semântica em silêncio, se a sobrecarga que foi escolhido através de inferência tinha outros argumentos que poderiam causar que ele seja classificado abaixo (por exemplo, argumentos opcionais com valores padrão, ou incompatibilidade de tipo entre declarados e argumento real que requer um implícito conversão). Em tal cenário, a resolução de sobrecarga não falhará, mas uma sobrecarga diferente será silenciosamente selecionada pelo compilador. Na prática, porém, é muito difícil de executar para esse caso sem construir cuidadosamente assinaturas de método para deliberadamente causar-lhe.

Converter uma implementação de interface implícita em uma explícita.

Kind of Break: Fonte e binário

Idiomas afetados: Todos

Isto é realmente apenas uma variação de mudar a acessibilidade de um método -. É apenas um pouco mais sutil, já que é fácil ignorar o fato de que nem todos o acesso a métodos de uma interface são necessariamente através de uma referência ao tipo da interface

API antes da mudança:

public class Foo : IEnumerable
{
    public IEnumerator GetEnumerator();
}

API Depois de Mudança:

public class Foo : IEnumerable
{
    IEnumerator IEnumerable.GetEnumerator();
}

O código do cliente Sample que as obras antes da mudança e é quebrado depois:

new Foo().GetEnumerator(); // fails because GetEnumerator() is no longer public

Converter uma implementação de interface explícita em um implícito.

Kind of Break: Fonte

Idiomas afetados: Todos

A refatoração de uma implementação de interface explícita em um implícito é mais sutil na forma como ele pode quebrar uma API. Na superfície, parece que este deve ser relativamente seguro, no entanto, quando combinada com a herança que pode causar problemas.

API antes da mudança:

public class Foo : IEnumerable
{
    IEnumerator IEnumerable.GetEnumerator() { yield return "Foo"; }
}

API Depois de Mudança:

public class Foo : IEnumerable
{
    public IEnumerator GetEnumerator() { yield return "Foo"; }
}

O código do cliente Sample que as obras antes da mudança e é quebrado depois:

class Bar : Foo, IEnumerable
{
    IEnumerator IEnumerable.GetEnumerator() // silently hides base instance
    { yield return "Bar"; }
}

foreach( var x in new Bar() )
    Console.WriteLine(x);    // originally output "Bar", now outputs "Foo"

Alterar um campo a uma propriedade

Kind of Break: API

Idiomas afetados: Visual Basic e C # *

Info: Quando você altera um campo normal ou variável em uma propriedade no Visual Basic, qualquer código fora referenciando esse membro de qualquer forma terá de ser recompilados.

API antes da mudança:

Public Class Foo    
    Public Shared Bar As String = ""    
End Class

API Depois de Mudança:

Public Class Foo
    Private Shared _Bar As String = ""
    Public Shared Property Bar As String
        Get
            Return _Bar
        End Get
        Set(value As String)
            _Bar = value
        End Set
    End Property
End Class    

código de cliente de exemplo que funciona, mas está quebrado depois:

Foo.Bar = "foobar"

Namespace Adição

quebra-level Fonte / fonte de nível de tranquilos semântica mudança

Devido às obras de resolução de maneira namespace em vb.Net, adicionando um namespace a uma biblioteca podem causar código Visual Basic que compilados com uma versão anterior da API não compilar com uma nova versão.

código de cliente Amostra:

Imports System
Imports Api.SomeNamespace

Public Class Foo
    Public Sub Bar()
        Dim dr As Data.DataRow
    End Sub
End Class

Se uma nova versão da API adiciona o Api.SomeNamespace.Data namespace, em seguida, o código acima não irá compilar.

Torna-se mais complicada com as importações de namespace em nível de projeto. Se Imports System é omitido do código acima, mas o namespace System é importado ao nível do projecto, em seguida, o código ainda pode resultar em um erro.

No entanto, se a API inclui um DataRow classe em seu namespace Api.SomeNamespace.Data, então o código irá compilar mas dr será uma instância de System.Data.DataRow quando compilado com a versão antiga da API e Api.SomeNamespace.Data.DataRow quando compilado com a nova versão da API .

Argumento Renomear

Fonte de nível ruptura

Mudando os nomes dos argumentos é uma alteração significativa em vb.net a partir da versão 7 (?) (Net versão 1?) E C # .NET da versão 4 (Net versão 4).

API antes da mudança:

namespace SomeNamespace {
    public class Foo {
        public static void Bar(string x) {
           ...
        }
    }
}

API após a mudança:

namespace SomeNamespace {
    public class Foo {
        public static void Bar(string y) {
           ...
        }
    }
}

código de cliente Amostra:

Api.SomeNamespace.Foo.Bar(x:"hi"); //C#
Api.SomeNamespace.Foo.Bar(x:="hi") 'VB

Ref Parâmetros

Fonte de nível ruptura

Adicionando uma substituição do método com a mesma assinatura, exceto que um parâmetro é passado por referência e não por valor causará fonte vb que as referências a API para ser incapaz de resolver a função. Visual Basic não tem nenhuma maneira (?) Para diferenciar esses métodos no ponto de chamada a menos que tenham diferentes nomes de argumentos, de modo tal mudança poderia causar ambos os membros para ser inutilizável a partir do código vb.

API antes da mudança:

namespace SomeNamespace {
    public class Foo {
        public static void Bar(string x) {
           ...
        }
    }
}

API após a mudança:

namespace SomeNamespace {
    public class Foo {
        public static void Bar(string x) {
           ...
        }
        public static void Bar(ref string x) {
           ...
        }
    }
}

código de cliente Amostra:

Api.SomeNamespace.Foo.Bar(str)

O campo de Propriedade Mudança

break-nível binário /-level Fonte pausa

Além do break-nível binário óbvio, isso pode causar uma ruptura de nível de origem se o membro é passada para um método por referência.

API antes da mudança:

namespace SomeNamespace {
    public class Foo {
        public int Bar;
    }
}

API após a mudança:

namespace SomeNamespace {
    public class Foo {
        public int Bar { get; set; }
    }
}

código de cliente Amostra:

FooBar(ref Api.SomeNamespace.Foo.Bar);

mudança API:

  1. Adicionando o atributo [Obsoleto] (você meio que cobriu isto com atributos mencionando;. No entanto, esta pode ser uma alteração de quebra quando se utiliza aviso-as-erro)

Binary-nível de quebra:

  1. Mover um tipo de uma montagem para outra
  2. Alterar o namespace de um tipo
  3. A adição de um tipo de classe de base a partir de um outro conjunto.
  4. A adição de um novo membro (evento protegido) que usa um tipo de outro assembly (Class2) como uma restrição argumento de modelo.

    protected void Something<T>() where T : Class2 { }
    
  5. A alteração de uma classe filha (Class3) derivar de um tipo em outro assembly quando a classe é usado como um argumento de modelo para esta classe.

    protected class Class3 : Class2 { }
    protected void Something<T>() where T : Class3 { }
    

-level Fonte tranquila semântica mudança:

  1. A adição / remoção / mudando substituições de Iguais (), GetHashCode (), ou ToString ()

(não tenho certeza onde estes fit)

Implantação muda:

  1. Adicionar / remover dependências / referências
  2. Atualização de dependências para versões mais recentes
  3. Alterar a 'plataforma de destino' entre x86, Itanium, x64, ou anycpu
  4. Construção / testando em um quadro diferente instalar (ou seja, a instalação de 3,5 em uma caixa de .Net 2.0 permite chamadas de API que então requerem Net 2.0 SP2)

Bootstrap / Configuração muda:

  1. Adicionar / Remover / Alterar as opções de configuração personalizada (ou seja, configurações App.config)
  2. Com o uso pesado de IoC / DI em aplicações de hoje, é algumas coisas necessário reconfigurar e / ou código de bootstrapping mudança para o código dependente DI.

Update:

Desculpe, eu não perceber que a única razão pela qual este estava quebrando para mim foi que eu usei-los em restrições de modelo.

Adicionando métodos de sobrecarga à morte uso de parâmetros padrão

Kind of pausa: Fonte de nível semântica tranquilos mudança

Porque as chamadas método transforma compilador com falta valores de parâmetro padrão para uma chamada explícita com o valor padrão no lado da chamada, a compatibilidade de código compilado existente é dada; um método com a assinatura correta será encontrada para todo o código previamente compilado.

Por outro lado, as chamadas sem o uso de parâmetros opcionais são agora compilados como uma chamada para o novo método que está faltando o parâmetro opcional. Tudo ainda está funcionando bem, mas se os chamados reside código em outro código de montagem, recém-compilado chamando agora é dependente para a nova versão desta montagem. Implantando montagens chamando o código refatorado sem também implantar os montagem reside o código refatorado em está resultando em "método não encontrado" exceções.

API antes da mudança

  public int MyMethod(int mandatoryParameter, int optionalParameter = 0)
  {
     return mandatoryParameter + optionalParameter;
  }    

API após a mudança

  public int MyMethod(int mandatoryParameter, int optionalParameter)
  {
     return mandatoryParameter + optionalParameter;
  }

  public int MyMethod(int mandatoryParameter)
  {
     return MyMethod(mandatoryParameter, 0);
  }

Exemplo de código que ainda estará trabalhando

  public int CodeNotDependentToNewVersion()
  {
     return MyMethod(5, 6); 
  }

Exemplo de código que está agora dependente para a nova versão ao compilar

  public int CodeDependentToNewVersion()
  {
     return MyMethod(5); 
  }

Mudar o nome de uma interface

kinda de Break: Fonte e binário

Idiomas afetados:. Provavelmente tudo, testado em C #

API antes da mudança:

public interface IFoo
{
    void Test();
}

public class Bar
{
    IFoo GetFoo() { return new Foo(); }
}

API Depois de Mudança:

public interface IFooNew // Of the exact same definition as the (old) IFoo
{
    void Test();
}

public class Bar
{
    IFooNew GetFoo() { return new Foo(); }
}

código de cliente de exemplo que funciona, mas está quebrado depois:

new Bar().GetFoo().Test(); // Binary only break
IFoo foo = new Bar().GetFoo(); // Source and binary break

sobrecarga de método com um parâmetro de tipo anulável

Tipo: Fonte de nível ruptura

Idiomas afetados: C #, VB

API antes de uma mudança:

public class Foo
{
    public void Bar(string param);
}

API após a mudança:

public class Foo
{
    public void Bar(string param);
    public void Bar(int? param);
}

Amostra de trabalho de código do cliente antes da mudança e quebrado depois que:

new Foo().Bar(null);

excepção:. A chamada é ambígua entre os seguintes métodos ou propriedades

Promoção para um método de extensão

Tipo: break-nível de fonte

Idiomas afetados: (? Talvez outros) C # v6 e superior

API antes da mudança:

public static class Foo
{
    public static void Bar(string x);
}

API após a mudança:

public static class Foo
{
    public void Bar(this string x);
}

Amostra de trabalho de código do cliente antes da mudança e quebrado depois que:

using static Foo;

class Program
{
    static void Main() => Bar("hello");
}

Mais informações: https://github.com/dotnet/csharplang/issues/665

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top