Cambio de las reglas de compatibilidad
A lo largo de su historia, .NET ha intentado mantener un alto nivel de compatibilidad de una versión a otra y entre sus implementaciones. Aunque .NET 5 (y .NET Core) y versiones posteriores se pueden considerar una nueva tecnología en comparación con .NET Framework, hay dos factores principales que limitan la capacidad de .NET para desviarse de .NET Framework:
Un gran número de desarrolladores desarrollaron originalmente o continúan desarrollando aplicaciones .NET Framework. Esperan un comportamiento coherente en las implementaciones de .NET.
Los proyectos de bibliotecas de .NET Standard permiten a los desarrolladores crear bibliotecas dirigidas a las API comunes compartidas por .NET Framework y .NET 5 (y .NET Core), versiones posteriores. Los desarrolladores esperan que una biblioteca que se usa en una aplicación de .NET 5 se comporte de forma idéntica a la misma biblioteca utilizada en una aplicación de .NET Framework.
Junto con la compatibilidad entre las implementaciones de .NET, los desarrolladores esperan un alto nivel de compatibilidad entre las versiones de una implementación determinada de .NET. En concreto, el código escrito para una versión anterior de .NET Core debería funcionar sin problemas en .NET 5 o una versión posterior. De hecho, muchos desarrolladores esperan que las nuevas API que se encuentran en las versiones más recientes de .NET también sean compatibles con las versiones preliminares en las que se han presentado esas API.
En este artículo se describen los cambios que afectan a la compatibilidad y el modo en el que el equipo de .NET evalúa cada tipo de cambio. El hecho de entender cómo el equipo de .NET aborda los posibles cambios importantes es particularmente útil para los desarrolladores que abren solicitudes de incorporación de cambios con el objetivo modificar el comportamiento de las API de .NET existentes.
En las secciones siguientes se describen las categorías de los cambios realizados en las API de .NET y su impacto sobre la compatibilidad de las aplicaciones. Los cambios están permitidos (✔️), no permitidos (❌) o requieren un criterio y una evaluación de cuán predecible, obvio y coherente era el comportamiento anterior (❓).
Nota
- Además de servir como guía para evaluar los cambios en las bibliotecas de .NET, los desarrolladores de bibliotecas también pueden utilizar estos criterios para evaluar los cambios en sus bibliotecas que tienen como objetivo varias implementaciones y versiones de .NET.
- Para obtener más información sobre las categorías de compatibilidad (por ejemplo, la compatibilidad con versiones anteriores y posteriores), consulte Cómo pueden afectar los cambios de código a la compatibilidad.
Modificaciones en el contrato público
Los cambios en esta categoría modifican el área expuesta pública de un tipo. No están permitidos la mayoría de los cambios en esta categoría ya que infringen la compatibilidad con versiones anteriores (la capacidad de una aplicación que se ha desarrollado con una versión anterior de una API para ejecutarse sin recompilación en una versión posterior).
Tipos
✔️ PERMITIDO: Supresión de una implementación de interfaz de un tipo cuando la interfaz ya está implementada por un tipo base
❓ REQUIERE EVALUACIÓN: Adición de una nueva implementación de interfaz a un tipo
Este es un cambio aceptable porque no afecta negativamente a los clientes existentes. Cualquier cambio en el tipo debe funcionar dentro de los límites de los cambios aceptables definidos aquí para que la nueva implementación siga siendo aceptable. Es necesario extremar las precauciones cuando se agregan interfaces que afectan directamente a la capacidad de un diseñador o serializador para generar código o datos que no se pueden consumir a un nivel inferior. Un ejemplo es la interfaz ISerializable.
❓ REQUIERE EVALUACIÓN: Introducción a una nueva clase base
Un tipo puede introducirse en una jerarquía entre dos tipos existentes si no introduce nuevos miembros de tipo abstract ni cambia la semántica o el comportamiento de los tipos existentes. Por ejemplo, en .NET Framework 2.0, la clase DbConnection se ha convertido en una nueva clase base para SqlConnection, que anteriormente había derivado directamente de Component.
✔️ PERMITIDO: Traslado de un tipo de un ensamblado a otro
El ensamblado anterior debe estar marcado con el TypeForwardedToAttribute que apunta al nuevo ensamblado.
✔️ PERMITIDO: Cambio de un tipo struct a un tipo
readonly struct
No se permite cambiar un tipo
readonly struct
a un tipostruct
.✔️ PERMITIDO: Adición de la palabra clave sealed o abstract a un tipo cuando hay ningún constructor accesible (público o protegido)
✔️ PERMITIDO: Expansión de la visibilidad de un tipo
❌NO PERMITIDO: Cambio del espacio de nombres o del nombre de un tipo
❌NO PERMITIDO: Cambio de nombre o eliminación de un tipo público
Esto interrumpe todo el código que utiliza el tipo cuyo nombre se ha cambiado o quitado.
Nota
En raras ocasiones, .NET puede quitar una API pública. Para más información, consulte Eliminación de API en .NET. Para obtener más información sobre la directiva de soporte de .NET, consulte Directiva de soporte de .NET.
❌NO PERMITIDO: Cambio del tipo subyacente de una enumeración
Se trata de un cambio importante en tiempo de compilación y de comportamiento, así como de un cambio importante binario que puede hacer que los argumentos de atributos no se puedan analizar.
❌NO PERMITIDO: Sellado de un tipo que anteriormente no estaba sellado
❌NO PERMITIDO: Adición de una interfaz al conjunto de tipos base de una interfaz
Si una interfaz implementa una interfaz que antes no se implementaba, todos los tipos que implementaron la versión original de la interfaz se interrumpen.
❓ REQUIERE EVALUACIÓN: Eliminación de una clase del conjunto de clases base o una interfaz desde el conjunto de interfaces implementadas
Hay una excepción a la regla para la eliminación de interfaces: puede agregar la implementación de una interfaz que se derive de la interfaz eliminada. Por ejemplo, puede quitar IDisposable si el tipo o interfaz ahora implementa IComponent, que implementa IDisposable.
❌NO PERMITIDO: Cambio de un tipo
readonly struct
a un tipo structNo obstante, se permite el cambio de un tipo
struct
a un tiporeadonly struct
.❌NO PERMITIDO: Cambio de un tipo struct a un tipo
ref struct
y viceversa❌NO PERMITIDO: Reducción de la visibilidad de un tipo
Sin embargo, se permite aumentar la visibilidad de un tipo.
Miembros
✔️ PERMITIDO: Expansión de la visibilidad de un miembro que no es virtual
✔️ PERMITIDO: Adición de un miembro de tipo abstract a un tipo público que no tiene ningún constructor accesible (público o protegido) o el tipo sealed
Sin embargo, no se permite agregar un miembro de tipo abstract a un tipo que tenga constructores accesibles (públicos o protegidos) y que no sea
sealed
.✔️ PERMITIDO: Restricción de la visibilidad de un miembro protected cuando el tipo no tiene constructores accesibles (públicos o protegidos) o el tipo es sealed
✔️ PERMITIDO: Desplazamiento de un miembro a una clase superior en la jerarquía que el tipo del que fue eliminado
✔️ PERMITIDO: Adición o eliminación de una invalidación
La introducción de una invalidación puede hacer que los consumidores anteriores omitan la invalidación cuando llamen a la base.
✔️ PERMITIDO: Adición de un constructor a una clase, junto con un constructor sin parámetros si la clase no tenía previamente constructores
Sin embargo, no se permite agregar un constructor a una clase que antes no tenía constructores sin agregar el constructor sin parámetros.
✔️ PERMITIDO: Cambio de un miembro de tipo abstract a virtual
✔️ PERMITIDO: Cambio de un valor devuelto
ref readonly
a unref
(excepto para los métodos o interfaces virtuales)✔️ PERMITIDO: Eliminación de readonly desde un campo, a menos que el tipo estático del campo sea un tipo de valor mutable
✔️ PERMITIDO: Llamada a un nuevo evento que no se ha definido anteriormente
❓ REQUIERE EVALUACIÓN: Adición de un nuevo campo de instancia a un tipo
Este cambio afecta a la serialización.
❌NO PERMITIDO: Cambio de nombre o eliminación de un miembro o parámetro público
Esto interrumpe todo el código que utiliza el miembro o parámetro cuyo nombre se ha cambiado o quitado.
Esto incluye el cambio de nombre o eliminación de un captador o establecedor de una propiedad, así como el cambio de nombre o eliminación de los miembros de la enumeración.
❌NO PERMITIDO: Adición de un miembro a una interfaz
Si proporciona una implementación, al agregar un nuevo miembro a una interfaz existente no se producirán necesariamente errores de compilación en los ensamblados de nivel inferior. Sin embargo, no todos los lenguajes admiten miembros de interfaz predeterminados (DIM). Además, en algunos escenarios, el entorno de ejecución no puede decidir qué miembro de interfaz predeterminado invocar. Por estos motivos, agregar un miembro a una interfaz existente se considera un cambio importante.
❌NO PERMITIDO: Cambio del valor de un miembro constante o de enumeración público
❌NO PERMITIDO: Cambio del tipo de una propiedad, campo, parámetro o valor devuelto
❌NO PERMITIDO: Adición, eliminación o cambio del orden de los parámetros
❌NO PERMITIDO: Adición o eliminación de la palabra clave in, out o ref en un parámetro
❌NO PERMITIDO: Cambio de nombre de un parámetro (incluido el cambio de mayúsculas y minúsculas)
Esto se considera importante por dos motivos:
Interrumpe los escenarios de enlace en tiempo de ejecución, como la característica de enlace en tiempo de ejecución en Visual Basic y dinámica en C#.
Interrumpe la compatibilidad del origen cuando los desarrolladores utilizan los argumentos con nombre.
❌NO PERMITIDO: Cambio de un valor devuelto
ref
a un valor devueltoref readonly
❌️ NO PERMITIDO: Cambio de un valor devuelto
ref readonly
a un valor devueltoref
en una interfaz o método virtual❌NO PERMITIDO: Adición o eliminación del tipo abstract de un miembro
❌NO PERMITIDO: Eliminación de la palabra clave virtual de un miembro
❌NO PERMITIDO: Adición de la palabra clave virtual a un miembro
Aunque esto a menudo no es un cambio importante porque el compilador de C# tiende a emitir las instrucciones de lenguaje intermedio (IL) callvirt para llamar a métodos no virtuales (
callvirt
realiza una comprobación nula, mientras que una llamada normal no lo hace), este comportamiento no es invariable por varias razones:C# no es el único lenguaje al que se dirige .NET.
El compilador de C# intenta cada vez más optimizar
callvirt
para una llamada normal cuando el método de destino no es virtual y probablemente no es nulo (como un método al que se accede a través del operador de propagación nula ?).
Convertir un método en virtual significa que el código del consumidor a menudo acabaría llamándolo no virtual.
❌NO PERMITIDO: Conversión de un miembro virtual en tipo abstract
Un miembro virtual proporciona una implementación del método que se puede reemplazar por una clase derivada. Un miembro abstract no proporciona ninguna implementación y debe ser reemplazado.
❌NO PERMITIDO: Adición de la palabra clave sealed a un miembro de interfaz
Agregar
sealed
a un miembro de interfaz predeterminado hará que no sea virtual, lo que impide que se llame a la implementación de un tipo derivado de ese miembro.❌NO PERMITIDO: Adición de un miembro de tipo abstract a un tipo público que tiene constructores accesibles (públicos o protegidos) y que no es de tipo sealed
❌NO PERMITIDO: Adición y eliminación de la palabra clave static de un miembro
❌NO PERMITIDO: Adición de una sobrecarga que impide una sobrecarga existente y define un comportamiento diferente
Esto interrumpe a los clientes existentes que se enlazaron a la sobrecarga anterior. Por ejemplo, si una clase tiene una versión única de un método que acepta un UInt32, un consumidor existente se enlazará correctamente a esa sobrecarga al pasar un valor Int32. Sin embargo, si agrega una sobrecarga que acepte un Int32, al volver a compilar o utilizar la característica de enlace en tiempo de ejecución, el compilador se enlaza ahora a la nueva sobrecarga. Si se produce un comportamiento diferente, se trata de un cambio importante.
❌NO PERMITIDO: Adición de un constructor a una clase que antes no tenía constructores sin agregar el constructor sin parámetros
❌️ NO PERMITIDO: Adición de readonly a un campo
❌NO PERMITIDO: Reducción de la visibilidad de un miembro
Esto incluye la reducción de la visibilidad de un miembro protected cuando hay constructores accesible (
public
oprotected
) y el tipo no es de tipo sealed. Si no es así, se permite reducir la visibilidad de un miembro protegido.Se permite aumentar la visibilidad de un miembro.
❌NO PERMITIDO: Cambio del tipo de un miembro
El valor devuelto de un método o el tipo de propiedad o campo no se pueden modificar. Por ejemplo, la firma de un método que devuelve un Object no se puede cambiar para devolver un String, o viceversa.
❌NO PERMITIDO: agregar un campo de instancia a una estructura que no tiene campos no públicos
Si una estructura solo tiene campos públicos o no tiene campos en absoluto, los autores de llamadas pueden declarar variables locales de ese tipo de estructura sin llamar al constructor de la estructura o inicializar primero el local en
default(T)
, siempre y cuando todos los campos públicos se establezcan en la estructura antes de su primer uso. Agregar todos los campos nuevos (públicos o no públicos) a este tipo de estructura es un cambio importante de origen para estos autores de llamadas, ya que el compilador ahora requerirá que se inicialicen los campos adicionales.Además, agregar cualquier campo nuevo (público o no público) a una estructura sin campos o solo con campos públicos es un cambio importante binario en los autores de llamadas que han aplicado
[SkipLocalsInit]
a su código. Dado que el compilador no era consciente de estos campos en tiempo de compilación, podía emitir IL que no inicializa completamente la estructura, lo que provocaba que la estructura se creara a partir de datos de pila no inicializados.Si una estructura tiene campos no públicos, el compilador ya aplica la inicialización mediante el constructor o
default(T)
, y agregar nuevos campos de instancia no es un cambio importante.❌NO PERMITIDO: Desencadenamiento de un evento existente nunca antes desencadenado
Cambios de comportamiento
Ensamblados
✔️ PERMITIDO: Portabilidad de un ensamblado cuando se siguen admitiendo las mismas plataformas
❌NO PERMITIDO: Cambio de nombre de un ensamblado
❌NO PERMITIDO: Cambio de la clave pública de un ensamblado
Propiedades, campos, parámetros y valores devueltos
✔️ PERMITIDO: Cambio del valor de una propiedad, un campo, un valor devuelto o del parámetro out a un tipo más derivado
Por ejemplo, un método que devuelve un tipo de Object puede devolver una instancia de String. (Sin embargo, no se puede cambiar la firma del método).
✔️ PERMITIDO: Aumento del intervalo de valores aceptados para una propiedad o parámetro si el miembro no es virtual
Mientras que el rango de valores que se pueden pasar al método o que se devuelven por el miembro puede expandirse, el parámetro o tipo de miembro no pueden. Por ejemplo, mientras que los valores pasados a un método pueden expandirse de 0-124 a 0-255, el tipo de parámetro no puede cambiar de Byte a Int32.
❌NO PERMITIDO: Aumento del intervalo de valores aceptados para una propiedad o parámetro si el miembro es virtual
Este cambio interrumpe los miembros invalidados existentes, que no funcionarán correctamente para la gama extendida de valores.
❌NO PERMITIDO: Disminución del intervalo de valores aceptados para una propiedad o parámetro
❌NO PERMITIDO: Aumento del intervalo de valores devueltos para una propiedad, un campo, un valor devuelto o el parámetro out
❌NO PERMITIDO: Cambio de los valores devueltos para una propiedad, un campo, un valor devuelto del método o el parámetroout
❌NO PERMITIDO: Cambio del valor predeterminado de una propiedad, un campo o un parámetro
Cambio o eliminación de un valor predeterminado del parámetro no es una interrupción binaria. La eliminación de un valor predeterminado del parámetro es una interrupción de origen y el cambio de un valor predeterminado del parámetro podría dar lugar a una interrupción de comportamiento después de la recompilación.
Por este motivo, la eliminación de valores predeterminados del parámetro es aceptable en el caso específico de "mover" esos valores predeterminados a una nueva sobrecarga del método para eliminar la ambigüedad. Por ejemplo, considere un método existente
MyMethod(int a = 1)
. Si presenta una sobrecarga deMyMethod
con dos parámetros opcionalesa
yb
, puede conservar la compatibilidad moviendo el valor predeterminado dea
a la nueva sobrecarga. Ahora las dos sobrecargas sonMyMethod(int a)
yMyMethod(int a = 1, int b = 2)
. Este patrón permite queMyMethod()
se ocupe de la compilación.❌NO PERMITIDO: Cambio de la precisión de un valor devuelto numérico
❓ REQUIERE EVALUACIÓN: Un cambio en el análisis de la entrada y el inicio de nuevas excepciones (incluso si el comportamiento de análisis no está especificado en la documentación)
Excepciones
✔️ PERMITIDO: Inicio de una excepción más derivada que una excepción existente
Debido a que la nueva excepción es una subclase de una excepción existente, el código de tratamiento de excepciones anterior continúa controlando la excepción. Por ejemplo, en .NET Framework 4, los métodos de creación y recuperación de la referencia cultural comenzaron a iniciar un CultureNotFoundException en lugar de un ArgumentException si no se podía encontrar la referencia cultural. Dado que CultureNotFoundException procede de ArgumentException, se trata de un cambio aceptable.
✔️ PERMITIDO: Inicio de una excepción más específica que NotSupportedException, NotImplementedException, NullReferenceException
✔️ PERMITIDO: Inicio de una excepción que se considera irrecuperable
Las excepciones irrecuperables no deben capturarse, sino que deben tratarse por un controlador general de alto nivel. Por lo tanto, no se espera que los usuarios tengan un código que capte estas excepciones explícitas. Las excepciones irrecuperables son:
✔️ PERMITIDO: Inicio de una nueva excepción en una nueva ruta del código
La excepción debe aplicarse solo a una nueva ruta del código que se ejecute con nuevos valores o estados de parámetros, y que no se pueda ejecutar por código existente que apunte a la versión anterior.
✔️ PERMITIDO: Eliminación de una excepción para permitir un comportamiento más sólido o nuevos escenarios
Por ejemplo, un método
Divide
que anteriormente solo trataba valores positivos e iniciaba un ArgumentOutOfRangeException de lo contrario, puede cambiarse para admitir tanto valores negativos como positivos sin iniciar una excepción.✔️ PERMITIDO: Cambio del texto de un mensaje de error
Los desarrolladores no deben confiar en el texto de los mensajes de error, que también cambian en función de la referencia cultural del usuario.
❌NO PERMITIDO: Inicio de una excepción en cualquier otro caso no enumerado anteriormente
❌NO PERMITIDO: Eliminación de una excepción en cualquier otro caso no enumerado anteriormente
Atributos
✔️ PERMITIDO: Cambio del valor de un atributo que no es observable
❌NO PERMITIDO: Cambio del valor de un atributo que es observable
❓ REQUIERE EVALUACIÓN: Eliminación de un atributo
En la mayoría de los casos, la eliminación de un atributo (como NonSerializedAttribute) es un cambio importante.
Compatibilidad con la plataforma
✔️ PERMITIDO: Admisión de una operación en una plataforma que antes no era posible
❌NO PERMITIDO: No admitir o requerir ahora un Service Pack específico para una operación que estaba admitida en una plataforma
Cambios de implementación internos
❓ REQUIERE EVALUACIÓN: Cambio del área expuesta de un tipo interno
Por lo general se permiten estos cambios, aunque interrumpan la reflexión privada. En algunos casos, cuando las bibliotecas populares de terceros o un gran número de desarrolladores dependen de las API internas, es posible que no se permitan dichos cambios.
❓ REQUIERE EVALUACIÓN: Cambio de la implementación interna de un miembro
Por lo general se permiten estos cambios, aunque interrumpan la reflexión privada. En algunos casos, cuando el código del cliente depende con frecuencia de una reflexión privada o cuando el cambio introduce efectos secundarios no deseados, estos cambios pueden no estar permitidos.
✔️ PERMITIDO: Mejora del rendimiento de una operación
La capacidad de modificar el rendimiento de una operación es esencial, pero tales cambios pueden interrumpir el código que depende de la velocidad actual de una operación. Esto es particularmente cierto en el caso del código que depende de la sincronización de las operaciones asincrónicas. El cambio en el rendimiento no debería tener ningún efecto en otro comportamiento de la API en cuestión; de lo contrario, el cambio se considerará importante.
✔️ PERMITIDO: Modificación indirecta (y a menudo de forma adversa) del rendimiento de una operación
Si el cambio en cuestión no está clasificado como importante por algún otro motivo, esto es aceptable. A menudo, es necesario tomar medidas que pueden incluir operaciones adicionales o que agregan nueva funcionalidad. Esto casi siempre afectará al rendimiento, pero puede ser esencial para que la API en cuestión funcione como se esperaba.
❌NO PERMITIDO: Cambio de una API sincrónica en asincrónica (y viceversa)
Cambios en el código
✔️ PERMITIDO: Adición de params a un parámetro
❌NO PERMITIDO: Cambio de un tipo struct a un tipo class y viceversa
❌NO PERMITIDO: Adición de la palabra clave checked a un bloque de código
Este cambio puede causar que el código que se ejecutó previamente inicie un OverflowException y es inaceptable.
❌NO PERMITIDO: Elimintación de params de un parámetro
❌NO PERMITIDO: Cambio del orden en el que se desencadenan los eventos
Los desarrolladores pueden esperar razonablemente que los eventos se desencadenen en el mismo orden, y el código de desarrollador depende frecuentemente del orden en el que se desencadenen los eventos.
❌NO PERMITIDO: Eliminación del inicio de un evento en una acción determinada
❌NO PERMITIDO: Cambio del número de veces que se llaman los eventos dados
❌NO PERMITIDO: Adición de FlagsAttribute a un tipo de enumeración