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.
fuente
Respuestas:
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
API después del cambio
Código de cliente de muestra que funciona antes del cambio
fuente
bar
.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
API después del cambio
Código de cliente de muestra que se rompe después
El código del cliente debe ser recompilado en
Foo(5, null)
el nivel de bytecode. El ensamblado llamado solo contendráFoo(int, string)
, noFoo(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 #).fuente
Func<int> f = Foo;
// esto fallará con la firma modificadaEste 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:
API después del cambio:
Código de muestra que sigue funcionando durante todo el cambio (aunque esperaba que se rompiera):
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
BarOverride
anula enFoo::Bar
lugar de hacerloFooBase::Bar
, el cargador de ensamblaje es lo suficientemente inteligente como para sustituir uno por otro correctamente sin ninguna queja, aparentemente, el hecho de queFoo
es una clase es lo que hace la diferencia. Imagínate...fuente
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:
API después del cambio:
Código de cliente de muestra que se rompe por cambio a nivel de origen:
Código de cliente de muestra que se rompe por cambio a nivel binario;
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.
fuente
Implements IFoo.Bar
, ¿ hará referencia transparenteIFooBase.Bar
?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
API después del cambio
Código de cliente de muestra que funciona pero que se rompe después:
fuente
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:
API después del cambio:
Código de cliente de muestra que se rompe por cambio:
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
Foo
yBar
en 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.
fuente
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:
API después del cambio:
Código de cliente de muestra que funciona antes del cambio y se rompe después:
fuente
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:
API después del cambio:
Código de cliente de muestra que funciona antes del cambio y se rompe después:
fuente
Foo
no tenía un método público llamadoGetEnumerator
, y está llamando al método a través de una referencia de tipoFoo
... .yield return "Bar"
:) pero sí, veo a dónde va esto ahora:foreach
siempre llama al método público llamadoGetEnumerator
, incluso si no es la implementación realIEnumerable.GetEnumerator
. Esto parece tener un ángulo más: incluso si tiene solo una clase, y se implementaIEnumerable
explícitamente, esto significa que es un cambio de ruptura de la fuente agregar un método público llamadoGetEnumerator
así, porque ahoraforeach
usará ese método sobre la implementación de la interfaz. Además, el mismo problema es aplicable a laIEnumerator
implementación ...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:
API después del cambio:
Código de cliente de muestra que funciona pero que se rompe después:
fuente
out
y losref
argumentos de los métodos no pueden usarse , a diferencia de los campos, y no pueden ser el objetivo del&
operador unario .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:
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. Si
Imports System
se omite del código anterior, pero elSystem
espacio 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
DataRow
en suApi.SomeNamespace.Data
espacio de nombres, el código se compilará perodr
será una instancia deSystem.Data.DataRow
cuando se compila con la versión anterior de la API yApi.SomeNamespace.Data.DataRow
cuando 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:
API después del cambio:
Código de cliente de muestra:
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:
API después del cambio:
Código de cliente de muestra:
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:
API después del cambio:
Código de cliente de muestra:
fuente
Cambio de API:
Descanso a nivel binario:
Agregar un nuevo miembro (evento protegido) que usa un tipo de otro ensamblado (Clase2) como restricción de argumento de plantilla.
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.
Cambio de semántica silenciosa a nivel de fuente:
(no estoy seguro de dónde encajan)
Cambios de implementación:
Bootstrap / Cambios de configuración:
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.
fuente
TypeForwardedToAttribute
se usa.-Werror
fuerces 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.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
API después del cambio
Código de muestra que seguirá funcionando
Código de muestra que ahora depende de la nueva versión al compilar
fuente
Renombrar una interfaz
Un poco de descanso: fuente y binario
Idiomas afectados: muy probablemente todos, probados en C #.
API antes del cambio:
API después del cambio:
Código de cliente de muestra que funciona pero que se rompe después:
fuente
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:
API después del cambio:
Ejemplo de código de cliente que funciona antes del cambio y se rompe después:
Excepción: la llamada es ambigua entre los siguientes métodos o propiedades.
fuente
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:
API después del cambio:
Ejemplo de código de cliente que funciona antes del cambio y se rompe después:
Más información: https://github.com/dotnet/csharplang/issues/665
fuente