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 tipo struct.

  • ✔️ 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 struct

    No obstante, se permite el cambio de un tipo struct a un tipo readonly 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 un ref (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:

  • NO PERMITIDO: Cambio de un valor devuelto ref a un valor devuelto ref readonly

  • ❌️ NO PERMITIDO: Cambio de un valor devuelto ref readonly a un valor devuelto ref 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 o protected) 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 de MyMethod con dos parámetros opcionales a y b, puede conservar la compatibilidad moviendo el valor predeterminado de a a la nueva sobrecarga. Ahora las dos sobrecargas son MyMethod(int a) y MyMethod(int a = 1, int b = 2). Este patrón permite que MyMethod() 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

Consulte también