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_Explicit
y op_Implicit
mé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 ExtensionMethodAttribute
y requiere que los métodos de extensión CLS se llamen como métodos estáticos.