Una guía definitiva para cambios que rompen la API en .NET

227

Me gustaría recopilar tanta información como sea posible con respecto al control de versiones de API en .NET / CLR, y específicamente cómo los cambios de API rompen o no las aplicaciones del cliente. Primero, definamos algunos términos:

Cambio de API : un cambio en la definición públicamente visible de un tipo, incluidos cualquiera de sus miembros públicos. Esto incluye cambiar el tipo y los nombres de los miembros, cambiar el tipo base de un tipo, agregar / eliminar interfaces de la lista de interfaces implementadas de un tipo, agregar / eliminar miembros (incluidas las sobrecargas), cambiar la visibilidad del miembro, cambiar el nombre del método y los parámetros de tipo, agregar valores predeterminados para parámetros de métodos, agregar / eliminar atributos en tipos y miembros, y agregar / eliminar parámetros de tipo genéricos en tipos y miembros (¿me perdí algo?). Esto no incluye ningún cambio en los organismos miembros ni ningún cambio en los miembros privados (es decir, no tenemos en cuenta Reflection).

Interrupción de nivel binario : un cambio de API que da como resultado que los ensamblados de clientes compilados en una versión anterior de la API no se carguen con la nueva versión. Ejemplo: cambiar la firma del método, incluso si permite que se llame de la misma manera que antes (es decir: nulo para devolver sobrecargas de valores predeterminados de tipo / parámetro).

Interrupción del nivel de origen : un cambio de API que da como resultado un código existente escrito para compilar con una versión anterior de la API que posiblemente no se compila con la nueva versión. Sin embargo, los ensamblados de clientes ya compilados funcionan como antes. Ejemplo: agregar una nueva sobrecarga que puede generar ambigüedad en las llamadas a métodos que no eran ambiguas anteriormente.

Cambio de semántica silenciosa a nivel de origen : un cambio de API que da como resultado que el código existente escrito para compilar con una versión anterior de la API cambie silenciosamente su semántica, por ejemplo, llamando a un método diferente. Sin embargo, el código debe continuar compilándose sin advertencias / errores, y los ensamblados compilados previamente deberían funcionar como antes. Ejemplo: implementar una nueva interfaz en una clase existente que resulta en una sobrecarga diferente elegida durante la resolución de sobrecarga.

El objetivo final es catalogar tantos cambios de API de semántica silenciosos y rotos como sea posible, y describir el efecto exacto de la rotura, y qué idiomas son y no son afectados por ella. Para ampliar este último: si bien algunos cambios afectan a todos los idiomas de manera universal (por ejemplo, agregar un nuevo miembro a una interfaz interrumpirá las implementaciones de esa interfaz en cualquier idioma), algunos requieren una semántica de lenguaje muy específica para entrar en juego y obtener un descanso. Esto generalmente implica la sobrecarga de métodos y, en general, cualquier cosa que tenga que ver con conversiones de tipo implícito. No parece haber ninguna forma de definir el "denominador menos común" aquí, incluso para lenguajes conformes con CLS (es decir, aquellos que se ajustan al menos a las reglas de "consumidor de CLS" como se define en las especificaciones de CLI), aunque yo ' Apreciaré que alguien me corrija por estar equivocado aquí, así que esto tendrá que ir idioma por idioma. Los de mayor interés son, naturalmente, los que vienen con .NET listo para usar: C #, VB y F #; pero otros, como IronPython, IronRuby, Delphi Prism, etc. también son relevantes. Cuanto más es un caso de esquina, más interesante será: cosas como eliminar miembros son bastante evidentes, pero las interacciones sutiles entre, por ejemplo, sobrecarga de métodos, parámetros opcionales / predeterminados, inferencia de tipo lambda y operadores de conversión pueden ser muy sorprendentes a veces.

Algunos ejemplos para comenzar esto:

Agregar nuevas sobrecargas de métodos

Tipo: ruptura a nivel fuente

Idiomas afectados: C #, VB, F #

API antes del cambio:

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

API después del cambio:

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

Ejemplo de código de cliente que funciona antes del cambio y se rompe después:

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

Agregar nuevas sobrecargas de operadores de conversión implícitas

Tipo: ruptura a nivel fuente.

Idiomas afectados: C #, VB

Idiomas no afectados: F #

API antes del cambio:

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

API después del cambio:

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

Ejemplo de código de cliente que funciona antes del cambio y se rompe después:

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

Notas: F # no está roto, ya que no tiene ningún soporte de nivel de lenguaje para operadores sobrecargados, ni explícito ni implícito, ambos deben llamarse directamente como op_Explicity op_Implicitmétodos.

Agregar nuevos métodos de instancia

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

Idiomas afectados: C #, VB

Idiomas no afectados: F #

API antes del cambio:

public class Foo
{
}

API después del cambio:

public class Foo
{
    public void Bar();
}

Ejemplo de código de cliente que sufre un cambio semántico silencioso:

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

new Foo().Bar();

Notas: F # no está roto, porque no tiene soporte de nivel de idioma ExtensionMethodAttributey requiere que los métodos de extensión CLS se llamen como métodos estáticos.

Pavel Minaev
fuente
Seguramente Microsoft ya cubre esto ... msdn.microsoft.com/en-us/netframework/aa570326.aspx
Robert Harvey
1
@Robert: su enlace trata sobre algo muy diferente: describe cambios de corte específicos en .NET Framework . Esta es una pregunta más amplia que describe patrones genéricos que pueden introducir cambios importantes en sus propias API (como autor de bibliotecas / marcos). No conozco ningún documento de MS que esté completo, aunque cualquier enlace a este, incluso si está incompleto, es definitivamente bienvenido.
Pavel Minaev
En alguna de estas categorías de "interrupción", ¿hay alguna en la que el problema solo sea evidente en el tiempo de ejecución?
Rohit
1
Sí, categoría "descanso binario". En ese caso, ya tiene un ensamblado de terceros compilado contra todas las versiones de su ensamblaje. Si coloca una nueva versión de su ensamblaje en el lugar, el ensamblaje de terceros deja de funcionar, o simplemente no se carga en tiempo de ejecución o funciona incorrectamente.
Pavel Minaev
3
Los agregaría en la publicación y comentarios blogs.msdn.com/b/ericlippert/archive/2012/01/09/…
Lukasz Madon el

Respuestas:

42

Cambiar una firma de método

Tipo: descanso de nivel binario

Idiomas afectados: C # (VB y F # lo 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 muestra que funciona antes del cambio

Foo.bar(13);
Justin Drury
fuente
15
De hecho, también puede ser una ruptura a nivel de fuente, si alguien intenta crear un delegado para bar.
Pavel Minaev
Eso también es cierto. Encontré este problema particular cuando hice algunos cambios en las utilidades de impresión en la aplicación de mi empresa. Cuando se lanzó la actualización, no todas las DLL que hacían referencia a estas utilidades se volvieron a compilar y lanzar, por lo que arroja una excepción no encontrada en el método.
Justin Drury
1
Esto se remonta al hecho de que los tipos de retorno no cuentan para la firma del método. Tampoco puede sobrecargar dos funciones basadas únicamente en el tipo de retorno. El mismo problema.
Jason Short
1
Subqestion a esta respuesta: ¿Alguien sabe la implicación de agregar un dotnet4 defaultvalue 'public static void bar (int i = 0);' o cambiando ese valor predeterminado de un valor a otro?
k3b
1
Para aquellos que van a aterrizar en esta página , creo que para C # (y "Creo" en la mayoría de los otros lenguajes OOP), los Tipos de retorno no contribuyen a la firma del método. Sí, la respuesta es correcta: los cambios de Firma contribuyen al cambio de nivel Binario. PERO el ejemplo no parece correcto En mi humilde opinión, el ejemplo correcto que puedo pensar es ANTES de la suma decimal pública (int a, int b) Después de la suma decimal pública (decimal a, decimal b) Favor de referirse a este enlace de MSDN 3.6 Firmas y sobrecarga
Bhanu Chhabra
40

Agregar un parámetro con un valor predeterminado.

Tipo de descanso: descanso de nivel binario

Incluso si el código fuente de la llamada no necesita cambiar, aún debe volver a compilarse (al igual que cuando se agrega un parámetro regular).

Esto se debe a que C # compila los valores predeterminados de los parámetros directamente en el ensamblado de llamada. Significa que si no vuelve a compilar, obtendrá una MissingMethodException porque el ensamblado anterior intenta llamar a un método con menos argumentos.

API antes del cambio

public void Foo(int a) { }

API después del cambio

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

Código de cliente de muestra que se rompe después

Foo(5);

El código del cliente debe ser recompilado en Foo(5, null)el nivel de bytecode. El ensamblado llamado solo contendrá Foo(int, string), no Foo(int). Esto se debe a que los valores de los parámetros predeterminados son puramente una característica del lenguaje, el tiempo de ejecución .Net no sabe nada sobre ellos. (Esto también explica por qué los valores predeterminados tienen que ser constantes de tiempo de compilación en C #).

Enigma de Eldritch
fuente
2
este es un cambio importante incluso para el nivel del código fuente: Func<int> f = Foo;// esto fallará con la firma modificada
Vagaus
26

Este no era muy obvio cuando lo descubrí, especialmente a la luz de la diferencia con la misma situación para las interfaces. No es un descanso en absoluto, pero es bastante sorprendente que decidí incluirlo:

Refactorizando miembros de clase en una clase base

Tipo: no es un descanso!

Idiomas afectados: ninguno (es decir, ninguno 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() {}
}

Código de muestra que sigue funcionando durante todo el cambio (aunque esperaba que se rompiera):

// 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 miembros de clase base virtual: "anulación explícita". Esperé que eso resultara en el mismo tipo de rotura que cuando se mueven los miembros de la interfaz a una interfaz base (ya que la IL generada para la anulación explícita es la misma que para la implementación explícita). Para mi sorpresa, este no es el caso, a pesar de que la IL generada aún especifica que se BarOverrideanula en Foo::Barlugar de hacerlo FooBase::Bar, el cargador de ensamblaje es lo suficientemente inteligente como para sustituir uno por otro correctamente sin ninguna queja, aparentemente, el hecho de que Fooes una clase es lo que hace la diferencia. Imagínate...

Pavel Minaev
fuente
3
Mientras la clase base esté en el mismo ensamblaje. De lo contrario, es un cambio de ruptura binario.
Jeremy
@Jeremy, ¿qué tipo de código se rompe en ese caso? ¿Se romperá el uso de Baz () por parte de una persona que llama externamente o es solo un problema con las personas que intentan extender Foo y anular Baz ()?
ChaseMedallion
@ChaseMedallion se está rompiendo si eres un usuario de segunda mano. Por ejemplo, la DLL compilada hace referencia a una versión anterior de Foo y usted hace referencia a esa DLL compilada, pero también usa una versión más nueva de la DLL de Foo. Se rompe con un extraño error, o al menos lo hizo para mí en las bibliotecas que he desarrollado antes.
Jeremy
19

Este es un caso especial quizás no tan obvio de "agregar / eliminar miembros de la interfaz", y pensé que merece su propia entrada a la luz de otro caso que voy a publicar a continuación. Entonces:

Refactorizando miembros de interfaz en una interfaz base

Tipo: saltos en los niveles de origen y binario

Idiomas afectados: C #, VB, C ++ / CLI, F # (para el corte de fuente; el binario naturalmente afecta cualquier idioma)

API antes del cambio:

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

API después del cambio:

interface IFooBase 
{
    void Bar();
}

interface IFoo : IFooBase
{
    void Baz();
}

Código de cliente de muestra que se rompe por cambio a nivel de origen:

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

Código de cliente de muestra que se rompe por cambio a nivel binario;

(new Foo()).Bar();

Notas:

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

La ruptura binaria se debe al hecho de que los métodos de interfaz están totalmente calificados en IL generado para implementaciones explícitas, y el nombre de la interfaz también debe ser exacto.

La implementación implícita donde esté disponible (es decir, C # y C ++ / CLI, pero no VB) funcionará bien tanto a nivel fuente como binario. Las llamadas a métodos tampoco se rompen.

Pavel Minaev
fuente
Eso no es cierto para todos los idiomas. Para VB no es un cambio de código fuente de última hora. Para C # lo es.
Jeremy
Entonces Implements IFoo.Bar, ¿ hará referencia transparente IFooBase.Bar?
Pavel Minaev
Sí, en realidad lo hace, puede hacer referencia a un miembro directa o indirectamente a través de la interfaz heredada cuando lo implementa. Sin embargo, esto siempre es un cambio binario de última hora.
Jeremy
15

Reordenando los valores enumerados

Tipo de descanso: cambio de semántica silenciosa a nivel de fuente / nivel binario

Idiomas afectados: todos

Reordenar los valores enumerados mantendrá la compatibilidad a nivel de fuente ya que los literales tienen el mismo nombre, pero sus índices ordinales se actualizarán, lo que puede causar algunos tipos de interrupciones silenciosas a nivel de fuente.

Peor aún son las interrupciones silenciosas de nivel binario que se pueden introducir si el código del cliente no se vuelve a compilar con la nueva versión de API. Los valores de enumeración son constantes de tiempo de compilación y, como tal, cualquier uso de ellos se integra en la IL del ensamblado del 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 cliente de muestra que funciona pero que se rompe después:

Foo.Bar < Foo.Baz
glopes
fuente
12

Esta es realmente una cosa muy rara en la práctica, pero no obstante sorprendente cuando sucede.

Agregar nuevos miembros no sobrecargados

Tipo: cambio de nivel de fuente o cambio semántico silencioso.

Idiomas afectados: C #, VB

Idiomas no afectados: F #, 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 muestra que se rompe por 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 causado por la inferencia de tipo lambda en C # y VB en presencia de una resolución de sobrecarga. Aquí se emplea una forma limitada de tipear patos para romper los lazos donde coincide más de un tipo, comprobando si el cuerpo de la lambda tiene sentido para un tipo dado; si solo un tipo da como resultado un cuerpo compilable, se elige ese.

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

Tenga en cuenta que los tipos Fooy Baren este ejemplo no están relacionados de ninguna manera, ni por herencia ni de otra manera. El simple uso de ellos en un solo grupo de métodos es suficiente para desencadenar esto, y si esto ocurre en el código del cliente, no tiene control sobre él.

El código de ejemplo anterior muestra una situación más simple en la que se trata de un corte de nivel de origen (es decir, resultados de error del compilador). Sin embargo, esto también puede ser un cambio de semántica silencioso, si la sobrecarga que se eligió por inferencia tenía otros argumentos que de lo contrario harían que se clasifique a continuación (por ejemplo, argumentos opcionales con valores predeterminados, o desajuste de tipo entre el argumento declarado y el real que requiere un implícito conversión). En tal escenario, la resolución de sobrecarga ya no fallará, pero el compilador seleccionará silenciosamente una sobrecarga diferente. En la práctica, sin embargo, es muy difícil encontrarse con este caso sin construir cuidadosamente firmas de métodos para causarlo deliberadamente.

Pavel Minaev
fuente
9

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

Tipo de descanso: fuente y binario

Idiomas afectados: todos

Esto es realmente solo una variación de cambiar la accesibilidad de un método, es 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 es necesariamente a través de una referencia al tipo de interfaz.

API antes del cambio:

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

API después del cambio:

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

Código de cliente de muestra que funciona antes del cambio y se rompe después:

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

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

Tipo de descanso: fuente

Idiomas afectados: todos

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

API antes del cambio:

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

API después del cambio:

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

Código de cliente de muestra que funciona antes del 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"
LBushkin
fuente
Lo siento, no lo sigo del todo: seguramente el código de muestra antes del cambio de API no se compilaría en absoluto, ya que antes el cambio Foono tenía un método público llamado GetEnumerator, y está llamando al método a través de una referencia de tipo Foo... .
Pavel Minaev
De hecho, intenté simplificar un ejemplo de memoria y terminó 'foobar' (perdón por el juego de palabras). Actualicé el ejemplo para demostrar correctamente el caso (y ser compilable).
LBushkin
En mi ejemplo, el problema está causado por algo más que la transición de un método de interfaz de ser implícito a ser público. Depende de la forma en que el compilador de C # determine qué método llamar en un bucle foreach. Dadas las reglas de resolución que el compilador ses, cambia de la versión en la clase derivada a la versión en la clase base.
LBushkin el
Olvidaste yield return "Bar":) pero sí, veo a dónde va esto ahora: foreachsiempre llama al método público llamado GetEnumerator, incluso si no es la implementación real IEnumerable.GetEnumerator. Esto parece tener un ángulo más: incluso si tiene solo una clase, y se implementa IEnumerableexplícitamente, esto significa que es un cambio de ruptura de la fuente agregar un método público llamado GetEnumeratorasí, porque ahora foreachusará ese método sobre la implementación de la interfaz. Además, el mismo problema es aplicable a la IEnumeratorimplementación ...
Pavel Minaev
6

Cambiar un campo a una propiedad

Tipo de descanso: API

Lenguajes afectados: Visual Basic y C # *

Información: cuando cambie un campo o variable normal en una propiedad en Visual Basic, cualquier código externo que haga referencia a ese miembro de alguna manera deberá volver a compilarse.

API antes del cambio:

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

API después del 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 muestra que funciona pero que se rompe después:

Foo.Bar = "foobar"
Hagelt18
fuente
2
En realidad, también rompería las cosas en C #, porque las propiedades outy los refargumentos de los métodos no pueden usarse , a diferencia de los campos, y no pueden ser el objetivo del &operador unario .
Pavel Minaev
5

Adición de espacio de nombres

Salto a nivel de fuente / cambio semántico silencioso a nivel de fuente

Debido a la forma en que funciona la resolución del espacio de nombres en vb.Net, agregar un espacio de nombres a una biblioteca puede hacer que el código de Visual Basic que se compiló con una versión anterior de la API no se compile con una nueva versión.

Código de cliente de muestra:

Imports System
Imports Api.SomeNamespace

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

Si una nueva versión de la API agrega el espacio de nombres Api.SomeNamespace.Data , entonces el código anterior no se compilará.

Se vuelve más complicado con las importaciones de espacio de nombres a nivel de proyecto. SiImports System se omite del código anterior, pero el Systemespacio de nombres se importa a nivel de proyecto, entonces el código aún puede generar un error.

Sin embargo, si la API incluye una clase DataRowen su Api.SomeNamespace.Dataespacio de nombres, el código se compilará pero drserá una instancia deSystem.Data.DataRow cuando se compila con la versión anterior de la API y Api.SomeNamespace.Data.DataRowcuando se compila con la nueva versión de la API.

Cambio de nombre de argumento

Salto a nivel de fuente

Cambiar los nombres de los argumentos es un cambio importante en vb.net desde la versión 7 (?) (.Net versión 1?) Y c # .net desde 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 muestra:

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

Parámetros de referencia

Salto a nivel de fuente

Agregar una anulación de método con la misma firma, excepto que un parámetro se pasa por referencia en lugar de por valor hará que la fuente vb que hace referencia a la API no pueda resolver la función. Visual Basic no tiene forma (?) De diferenciar estos métodos en el punto de llamada a menos que tengan nombres de argumentos diferentes, por lo que un cambio de este tipo podría hacer que ambos miembros sean inutilizables del 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 muestra:

Api.SomeNamespace.Foo.Bar(str)

Cambio de campo a propiedad

Salto a nivel binario / Salto a nivel fuente

Además del evidente salto de nivel binario, esto puede causar un salto de nivel de origen si el miembro se pasa a un método por 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 muestra:

FooBar(ref Api.SomeNamespace.Foo.Bar);
jswolf19
fuente
4

Cambio de API:

  1. Agregar el atributo [Obsoleto] (de alguna manera cubriste esto con la mención de los atributos; sin embargo, esto puede ser un cambio importante cuando se usa la advertencia como error).

Descanso a nivel binario:

  1. Mover un tipo de un ensamblaje a otro
  2. Cambiar el espacio de nombres de un tipo
  3. Agregar un tipo de clase base de otro ensamblado.
  4. Agregar un nuevo miembro (evento protegido) que usa un tipo de otro ensamblado (Clase2) como restricción de argumento de plantilla.

    protected void Something<T>() where T : Class2 { }
  5. Cambiar una clase secundaria (Class3) para derivar de un tipo en otro ensamblado cuando la clase se usa como argumento de plantilla para esta clase.

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

Cambio de semántica silenciosa a nivel de fuente:

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

(no estoy seguro de dónde encajan)

Cambios de implementación:

  1. Agregar / eliminar dependencias / referencias
  2. Actualización de dependencias a versiones más nuevas
  3. Cambiar la 'plataforma de destino' entre x86, Itanium, x64 o anycpu
  4. Crear / probar en una instalación de marco diferente (es decir, instalar 3.5 en un cuadro .Net 2.0 permite llamadas API que luego requieren .Net 2.0 SP2)

Bootstrap / Cambios de configuración:

  1. Agregar / quitar / cambiar opciones de configuración personalizadas (es decir, configuración de configuración de la aplicación)
  2. Con el uso intensivo de IoC / DI en las aplicaciones actuales, es necesario reconfigurar y / o cambiar el código de arranque para el código dependiente de DI.

Actualizar:

Lo siento, no me di cuenta de que la única razón por la que esto me estaba rompiendo era porque los usaba en restricciones de plantilla.

csharptest.net
fuente
"Agregar un nuevo miembro (evento protegido) que usa un tipo de otro ensamblado". - IIRC, el cliente solo necesita hacer referencia a los conjuntos dependientes que contienen tipos base de los conjuntos a los que ya hace referencia; no tiene que hacer referencia a ensamblajes que simplemente se usan (incluso si los tipos están en firmas de métodos); No estoy 100% seguro de esto. ¿Tiene una referencia para reglas precisas para esto? Además, mover un tipo puede no romperse si TypeForwardedToAttributese usa.
Pavel Minaev
Ese "TypeForderedTo" es una novedad para mí, lo comprobaré. En cuanto al otro, tampoco estoy al 100% ... déjame ver si puedo repro y actualizaré la publicación.
csharptest.net
Entonces, no -Werrorfuerces el sistema de compilación que envías con lanzamiento de tarballs. Este indicador es más útil para el desarrollador del código y, con mayor frecuencia, no es útil para el consumidor.
binki
@binki excelente punto, tratar las advertencias como errores debería ser suficiente solo en las compilaciones DEBUG.
csharptest.net
3

Agregar métodos de sobrecarga para eliminar el uso de parámetros predeterminados

Tipo de descanso: cambio de semántica silenciosa a nivel fuente

Debido a que el compilador transforma las llamadas a métodos con valores de parámetros predeterminados faltantes en una llamada explícita con el valor predeterminado en el lado de la llamada, se da compatibilidad para el código compilado existente; Se encontrará un método con la firma correcta para 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 al que le falta el parámetro opcional. Todo sigue funcionando bien, pero si el código llamado reside en otro ensamblado, el código recién compilado que lo llama ahora depende de la nueva versión de este ensamblado. La implementación de ensamblados que llaman al código refactorizado sin también desplegar el ensamblado en el que reside el código refactorizado da como resultado excepciones de "método no encontrado".

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 muestra que seguirá funcionando

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

Código de muestra que ahora depende de la nueva versión al compilar

  public int CodeDependentToNewVersion()
  {
     return MyMethod(5); 
  }
Usted no sabe
fuente
1

Renombrar una interfaz

Un poco de descanso: fuente y binario

Idiomas afectados: muy probablemente todos, probados en C #.

API antes del cambio:

public interface IFoo
{
    void Test();
}

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

API después del 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 muestra que funciona pero que se rompe después:

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

Método de sobrecarga con un parámetro de tipo anulable

Tipo: ruptura a nivel de fuente

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);
}

Ejemplo de código de cliente que funciona antes del cambio y se rompe después:

new Foo().Bar(null);

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

Bohdan Spilnyi
fuente
0

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

Tipo: ruptura a nivel fuente

Lenguajes afectados: C # v6 y superior (¿quizás otros?)

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);
}

Ejemplo de código de cliente que funciona antes del cambio y se rompe después:

using static Foo;

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

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

rory.ap
fuente