Pregunta

Me gustaría recopilar tanta información como sea posible sobre la API de control de versiones en .NET/CLR, y específicamente cómo los cambios en la API de hacer o de no romper las aplicaciones cliente.En primer lugar, vamos a definir algunos términos:

Cambio de API - un cambio en la visible al público la definición de un tipo, incluyendo cualquiera de sus miembros públicos.Esto incluye el cambio de tipo y los nombres de los miembros, el cambio de tipo base de un tipo, la adición/eliminación de interfaces de la lista de implementar las interfaces de un tipo, la adición/eliminación de los miembros (incluyendo sobrecargas), el cambio de los estados de visibilidad, el cambio de nombre del método y los parámetros de tipo, valores por defecto para los parámetros del método, añadir/quitar los atributos de los tipos y miembros, y la adición o extracción de parámetros de tipo genérico sobre los tipos y miembros (¿se me olvida algo?).Esto no incluye los cambios en los organismos miembros, o cualquier modificación a los miembros del sector privado (es decir,no tomamos en cuenta la Reflexión).

Binario-nivel de romper - un cambio de API que los resultados en el cliente ensamblados compilados en contra de la versión anterior de la API potencialmente no carga con la nueva versión.Ejemplo:el cambio de método de firma, incluso si se permite que se llama de la misma manera como antes (es decir:anular para volver y tipo de parámetro valor predeterminado valores de las sobrecargas).

A nivel de fuente de romper - un cambio de API que los resultados existentes en el código escrito para compilar en contra de la versión anterior de la API potencialmente no compilar con la nueva versión.Ya compilado cliente asambleas trabajo como antes, sin embargo.Ejemplo:la adición de una nueva sobrecarga que puede resultar en la ambigüedad en las llamadas a métodos que se inequívoco de la anterior.

A nivel de fuente de tranquilidad semántica de cambio - un cambio de API que los resultados existentes en el código escrito para compilar en contra de la versión anterior de la API tranquilamente cambiar su semántica, por ejemplo,llamando a un método diferente.El código, sin embargo, debería seguir para compilar con ningún tipo de advertencias/errores, y previamente ensamblados compilados debería funcionar como antes.Ejemplo:la implementación de una nueva interfaz en una clase existente que se traduce en una diferente de la sobrecarga de ser elegido, durante la resolución de sobrecarga.

El objetivo final es catalogize como muchos de ruptura y tranquilo semántica de cambios en el API como sea posible, y describir el efecto exacto de la rotura, y que los idiomas son y no son afectados por ella.Para ampliar sobre este último:mientras que algunos cambios afectan a todos los idiomas universalmente (por ejemplo,la adición de un nuevo miembro a una interfaz de romper las implementaciones de la interfaz en cualquier idioma), algunos requieren de muy específico del lenguaje semántica a entrar en juego para conseguir un descanso.Esto normalmente implica la sobrecarga de método, y, en general, cualquier cosa que tenga que ver con las conversiones de tipo implícito.Parece que no hay ninguna forma de definir el "mínimo común denominador" aquí, incluso para el CLS-conformes idiomas (es decir,estos conforme, al menos, a las reglas de "CLS consumidor", como se define en la CLI spec) - a pesar de que voy a apreciar si alguien me corrige como mal aquí - así que este tendrá que ir lenguaje por el lenguaje.Los de mayor interés son, naturalmente, los que vienen con .NETO de la caja:C#, VB y F#;pero otros, como IronPython, IronRuby, Delphi Prism etc también son relevantes.Más de una esquina caso, más interesante será - cosas como la eliminación de los miembros son bastante evidente, pero sutiles interacciones entre, por ejemplo,la sobrecarga de métodos, opcional/parámetros por defecto, lambda tipo de inferencia, y operadores de conversión puede ser muy sorprendente a veces.

Un par de ejemplos para activar este:

Añadir nuevo método de sobrecargas

Tipo:a nivel de fuente de romper

Idiomas afectadas:C#, VB, F#

API antes de cambiar:

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

API tras el cambio:

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

Ejemplo de cliente código de trabajo antes de cambiar y roto después de ella:

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

La adición de nuevos conversión implícita sobrecargas de operador

Tipo:a nivel de fuente de romper.

Idiomas afectadas:C#, VB

Los idiomas no se ven afectados:F#

API antes de cambiar:

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

API tras el cambio:

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

Ejemplo de cliente código de trabajo antes de cambiar y roto después de ella:

void Bar(int x);
void Bar(float x);
Bar(new Foo());

Notas:F# no está roto, porque no tiene ningún nivel de idioma de apoyo para los operadores sobrecargados, ni explícita ni implícita - ambos tienen que ser llamados directamente como op_Explicit y op_Implicit métodos.

La adición de nuevos métodos de instancia

Tipo:a nivel de fuente de tranquilidad semántica de cambio.

Idiomas afectadas:C#, VB

Los idiomas no se ven afectados:F#

API antes de cambiar:

public class Foo
{
}

API tras el cambio:

public class Foo
{
    public void Bar();
}

Ejemplo de código de cliente que sufre una tranquila semántica de cambio:

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

new Foo().Bar();

Notas:F# no está roto, porque no tiene el nivel de idioma de apoyo para ExtensionMethodAttribute, y requiere CLS métodos de extensión para ser llamado como métodos estáticos.

¿Fue útil?

Solución

Cambio de una firma de método

Tipo: nivel binario rotura

Idiomas afectados: C # (VB y F # más probable, pero no probado)

API antes del cambio

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

API después del cambio

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

código de cliente de la muestra de trabajo antes del cambio

Foo.bar(13);

Otros consejos

Adición de un parámetro con un valor por defecto.

tipo de ruptura: ruptura de nivel binario

Incluso si el código fuente de la llamada no tiene que cambiar, que todavía tiene que volver a compilar (al igual que cuando la adición de un parámetro normal).

Esto es debido a que C # compila los valores por defecto de los parámetros directamente en el conjunto de llamadas. Esto significa que si no se vuelve a compilar, obtendrá un MissingMethodException porque el viejo ensamblaje intenta llamar a un método con menos argumentos.

Antes de la API Cambiar

public void Foo(int a) { }

API Después de Cambio

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

código de cliente de la muestra que se ha roto después

Foo(5);

El código de cliente necesita volver a compilar en Foo(5, null) a nivel de código de bytes. La asamblea llamada sólo contendrá Foo(int, string), no Foo(int). Esto se debe a los valores predeterminados de los parámetros son puramente una característica del lenguaje, el tiempo de ejecución .Net no sabe nada de ellos. (Esto también explicaría por qué los valores por defecto tienen que ser constantes en tiempo de compilación en C #).

Este era muy no evidente cuando descubrí que, especialmente en vista de la diferencia con la misma situación para las interfaces. No es una ruptura en absoluto, pero es bastante sorprendente que decidí incluirlo:

miembros de la clase Refactoring en una clase base

Tipo: no un descanso

Idiomas afectados: ninguno (es decir, ninguno de ellos está roto)

API antes del cambio:

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

API después del cambio:

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

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

El código de ejemplo que sigue trabajando a lo largo del cambio (a pesar de que esperaba que se rompa):

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

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

Notas:

C ++ / CLI es el único lenguaje .NET que tiene una construcción análoga a la implementación de interfaz explícita para los miembros de la clase base virtuales - "anulación explícita". Que esperaba plenamente que resulte en la misma clase de rotura como cuando se mueve elementos de interfaz a una interfaz de base (ya que la IL generado para override explícito es el mismo que para la aplicación explícita). Para mi sorpresa, este no es el caso - a pesar generada IL todavía especifica que las anulaciones BarOverride Foo::Bar en lugar de FooBase::Bar, cargador de montaje es lo suficientemente inteligente como para sustituir uno por otro correctamente sin ninguna queja - al parecer, el hecho de que Foo es una clase es lo que hace la diferencia. Va la figura ...

Este es, tal vez no tan obvia caso especial de "agregar/eliminar miembros de la interfaz", y pensé que se merece su propia entrada en la luz de otro caso que voy a publicar a continuación.Así:

Refactorización de los miembros de la interfaz en una base de la interfaz

Tipo:se rompe en la fuente y binario niveles

Idiomas afectadas:C#, VB, C++/CLI, F# (para fuente break;binario, por supuesto, afecta a cualquier idioma)

API antes de cambiar:

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

API tras el cambio:

interface IFooBase 
{
    void Bar();
}

interface IFoo : IFooBase
{
    void Baz();
}

De la muestra código del cliente que es roto por el cambio en el nivel de la fuente:

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

De la muestra código del cliente que es roto por el cambio a nivel binario;

(new Foo()).Bar();

Notas:

Para el nivel de la fuente de romper, el problema es que C#, VB y C++/CLI, requieren exacto el nombre de la interfaz en la declaración de la interfaz de miembro de ejecución;por lo tanto, si el miembro es trasladado a una base de la interfaz, el código ya no se compilará.

Binario break es debido al hecho de que los métodos de interfaz está plenamente cualificado en genera IL explícita de las implementaciones, y el nombre de la interfaz también debe ser exacta.

Implícito la aplicación donde esté disponible (es decir,C# y C++/CLI, pero no VB) funcionará bien en el origen y nivel binario.Las llamadas de método no descanso bien.

valores Reordenación enumerado

tipo de ruptura: la semántica tranquilo / a nivel binario a nivel de fuente cambie

Idiomas afectada:

valores enumerados Reordenamiento mantendrán la compatibilidad a nivel de fuente como literales tienen el mismo nombre, pero sus índices ordinales serán actualizados, lo que puede causar algunos tipos de rupturas de nivel de fuente silenciosos.

Lo que es peor es los silenciosos se rompe a nivel binario que pueden introducirse si el código de cliente no se vuelve a compilar en contra de la nueva versión de la API. valores de enumeración son constantes de tiempo de compilación y como tal cualquier uso de ellas se cuecen en IL de la asamblea cliente. Este caso puede ser particularmente difícil de detectar a veces.

API antes del cambio

public enum Foo
{
   Bar,
   Baz
}

API después del cambio

public enum Foo
{
   Baz,
   Bar
}

Código de ejemplo de cliente que funciona, pero se rompe después:

Foo.Bar < Foo.Baz

Este es realmente una cosa muy rara en la práctica, pero sin embargo una sorprendente uno cuando sucede.

Adición de nuevos miembros no sobrecargado

Tipo:. Rotura nivel de la fuente o la semántica tranquilas cambio

Idiomas afectados: C #, VB

Las lenguas no afectados: C #, C ++ / CLI

API antes del cambio:

public class Foo
{
}

API después del cambio:

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

código de cliente de la muestra que se ha roto por el cambio:

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:

El problema aquí es causada por tipo lambda inferencia en C # y VB en presencia de resolución de sobrecarga. Se emplea una forma limitada de la tipificación de pato aquí para romper los lazos donde más de un tipo de los partidos, comprobando si el cuerpo de la lambda tiene sentido para un tipo dado -. Si sólo un tipo resultados en el cuerpo compilable, se elige que uno

El peligro aquí es que el código de cliente puede tener un grupo método sobrecargado donde algunos métodos toman argumentos de sus propios tipos, y otros toman argumentos de tipos expuestos por su biblioteca. Si cualquiera de su código, a continuación se basa en el algoritmo de inferencia de tipos para determinar el método correcto basado únicamente en presencia o ausencia de los miembros, a continuación, añadir un nuevo miembro a uno de sus tipos con el mismo nombre que en uno de los tipos de los clientes potenciales pueden lanzar inferencia off, lo que resulta en la ambigüedad durante la resolución de sobrecarga.

Tenga en cuenta que los tipos Foo y Bar en este ejemplo no están relacionados de alguna manera, no por herencia ni de otra manera. El simple uso de ellos en un solo grupo método es suficiente para desencadenar esto, y si esto ocurre en el código de cliente, usted no tiene ningún control sobre él.

El código de ejemplo anterior demuestra una situación más simple donde esta es una ruptura a nivel de fuente (es decir, resultados de error del compilador). Sin embargo, esto también puede ser un cambio semántico en silencio, si la sobrecarga que fue elegido a través de la inferencia tenía otros argumentos que de otro modo hacer que se ubicó por debajo (por ejemplo, argumentos opcionales con valores por defecto, o el tipo de desajuste entre declarado y argumento real que requiere una implícita conversión). En tal escenario, la resolución de sobrecarga ya no fallará, pero una sobrecarga diferente será seleccionado en silencio por el compilador. En la práctica, sin embargo, es muy difícil de ejecutar en este caso sin construir cuidadosamente firmas de métodos para causar deliberadamente.

Convertir una implementación de la interfaz implícita en una explícita.

tipo de ruptura: fuente y binario

Idiomas afectada:

Esto es realmente sólo una variación de cambiar de un método de accesibilidad -. Es sólo un poco más sutil, ya que es fácil pasar por alto el hecho de que no todo el acceso a los métodos de una interfaz son necesariamente a través de una referencia al tipo de la interfaz

Antes de API Cambio:

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

Después de API Cambio:

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

El código de cliente de la muestra que funciona antes de cambio y se rompe después:

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

Convertir una implementación de interfaz explícita en un implícito.

tipo de ruptura: Fuente

Idiomas afectada:

La refactorización de una implementación de interfaz explícita en un implícito es más sutil en la forma en que se puede romper una API. En la superficie, parecería que esto debería ser relativamente seguro, sin embargo, cuando se combina con la herencia que puede causar problemas.

Antes de API Cambio:

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

Después de API Cambio:

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

El código de cliente de la muestra que funciona antes de cambio y se rompe después:

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"

Cambio de un campo a una propiedad

tipo de ruptura: API

Idiomas afectadas: Visual Basic y C # *

Información: Cuando se cambia un campo normal o variable en una propiedad en Visual Basic, tendrá que volver a compilar cualquier código fuera referencia a ese miembro de ninguna manera.

API Antes de Cambio:

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

API Después de Cambio:

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 ejemplo que funciona, pero se rompe después:

Foo.Bar = "foobar"

Espacio de nombres Addition

semántica tranquilas quiebre a nivel de fuente / a nivel de fuente cambian

Debido a la forma en la resolución de espacio de nombres trabaja en vb.Net, la adición de un espacio de nombres a una biblioteca puede causar código de Visual Basic que se compiló con una versión anterior de la API para que no se compile con una nueva versión.

código de cliente de la muestra:

Imports System
Imports Api.SomeNamespace

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

Si hay una nueva versión de la API añade el Api.SomeNamespace.Data espacio de nombres, a continuación, el código anterior no se compilará.

Se vuelve más complicado, con las importaciones de espacio de nombres a nivel de proyecto. Si Imports System se omite del código anterior, pero el espacio de nombres System ha sido importada a nivel de proyecto, entonces el código todavía puede dar lugar a un error.

Sin embargo, si la API incluye un DataRow clase en su espacio de nombres Api.SomeNamespace.Data, a continuación, el código se compila pero dr será una instancia de System.Data.DataRow cuando se compila con la versión anterior de la API y Api.SomeNamespace.Data.DataRow cuando se compila con la nueva versión de la API .

Argumento Cambio de nombre

break Fuente de nivel

Cambio de los nombres de los argumentos es un cambio de última hora en vb.net partir de la versión 7 (?) (.Net versión 1?) Y C # .NET de la versión 4 (.Net versión 4).

API antes del cambio:

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

API después del cambio:

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

código de cliente de la muestra:

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

Ref Parámetros

break Fuente de nivel

Adición de un override método con la misma firma, excepto que un parámetro se pasa por referencia en lugar de por valor causará fuente vb que hace referencia a la API para ser incapaz de resolver la función. Visual Basic no tiene forma (?) Para diferenciar estos métodos al punto de llamada a menos que tengan diferentes nombres de argumentos, por lo que un cambio podría hacer que ambos miembros a ser inutilizables desde el código vb.

API antes del cambio:

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

API después del cambio:

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

código de cliente de la muestra:

Api.SomeNamespace.Foo.Bar(str)

El campo de la propiedad Cambiar

ruptura de nivel binario / break Fuente de nivel

Además de la ruptura de nivel binario evidente, esto puede causar una ruptura a nivel de fuente si el miembro se pasa a un método de referencia.

API antes del cambio:

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

API después del cambio:

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

código de cliente de la muestra:

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

cambio de API:

  1. Añadir el atributo [obsoleto] (que cubría un poco esto con atributos mencionan, sin embargo, esto puede ser un cambio importante cuando se utiliza la advertencia-como-error).

ruptura de nivel binario:

  1. Mover un tipo de un conjunto a otro
  2. Cambio del espacio de nombres de un tipo
  3. Adición de un tipo de clase de base de otro montaje.
  4. Adición de un nuevo miembro (evento protegido) que utiliza un tipo de otro conjunto (Class2) como una restricción argumento de plantilla.

    protected void Something<T>() where T : Class2 { }
    
  5. Cambio de una clase de niño (Class3) para derivar de un tipo en el otro conjunto cuando la clase se usa como un argumento de plantilla para esta clase.

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

Fuente de nivel de la semántica tranquilas cambio:

  1. Agregar / eliminar / cambiar las anulaciones de Iguales (), GetHashCode (), o ToString ()

(no sé donde estos encajan)

cambios de implementación:

  1. añadir / eliminar dependencias / referencias
  2. dependencias de actualizar a versiones más recientes
  3. Cambio de la 'plataforma de destino' entre x86, Itanium, x64, o Cualquier CPU
  4. Construcción / pruebas en un marco diferente instalar (es decir, la instalación de 3,5 en una caja de .Net 2.0 permite llamadas de API que entonces requiere .Net 2.0 SP2)

Bootstrap cambios / configuración:

  1. opciones Adición de Extracción de Cambio de configuración / / personalizado (es decir, la configuración de App.config)
  2. Con el uso intensivo de la COI / DI en las aplicaciones de hoy en día, es necesario configurar de nuevo tantos y / o cambiar el código de arranque para el código dependiente de DI.

Actualización:

Lo siento, no me di cuenta de que la única razón de que esto se rompía para mí fue que he usado en las limitaciones de la plantilla.

Adición de métodos de sobrecarga a la desaparición uso de los parámetros por defecto

tipo de ruptura: la semántica tranquilas a nivel de fuente cambiar

Debido a que el compilador transforma método llama al que le faltan valores de los parámetros por defecto para una llamada explícita con el valor por defecto en el lado llamante, la compatibilidad para los actuales se da código compilado; un método con la firma correcta se puede encontrar por todo el código compilado previamente.

Por otro lado, las llamadas sin el uso de parámetros opcionales ahora se compilan como una llamada al nuevo método que falta el parámetro opcional. Todo sigue trabajando bien, pero si el código de llamada reside en otra asamblea, recién código compilado llamando ahora depende de la nueva versión de este montaje. Implementación de conjuntos de llamada el código refactorizado sin también el despliegue de la Asamblea el código refactorizado reside en está resultando en "método no encontrado" excepciones.

API antes del cambio

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

API después del cambio

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

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

Código de ejemplo que todavía va a trabajar

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

código de ejemplo que depende ahora de la nueva versión al compilar

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

Cambiar el nombre de una interfaz

Un poco de descanso: Fuente y binario

Idiomas afectado:. Lo más probable es todo, probado en C #

API Antes de Cambio:

public interface IFoo
{
    void Test();
}

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

API Después de Cambio:

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 ejemplo que funciona, pero se rompe después:

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

sobrecarga de métodos con un parámetro de tipo anulable

Tipo: break Fuente de nivel

Idiomas afectados: C #, VB

API antes de un cambio:

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

API después del cambio:

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

código de cliente de la muestra de trabajo antes del cambio y roto después de que:

new Foo().Bar(null);

Excepción:. La llamada es ambigua entre los métodos o propiedades siguientes

Promoción a un método de extensión

Tipo: rotura a nivel de fuente

Idiomas afectados: (? Tal vez otros) C # v6 y mayores

API antes del cambio:

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

API después del cambio:

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

código de cliente de la muestra de trabajo antes del cambio y roto después de que:

using static Foo;

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

Más información: https://github.com/dotnet/csharplang/issues/665

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top