Ändra regler för kompatibilitet
Under hela sin historik har .NET försökt upprätthålla en hög kompatibilitetsnivå från version till version och mellan implementeringar av .NET. Även om .NET 5 -versioner (och .NET Core) och senare versioner kan betraktas som en ny teknik jämfört med .NET Framework, begränsar två viktiga faktorer möjligheten för den här implementeringen av .NET att avvika från .NET Framework:
Ett stort antal utvecklare utvecklade eller fortsätter att utveckla .NET Framework-program. De förväntar sig konsekvent beteende i .NET-implementeringar.
Med .NET Standard-biblioteksprojekt kan utvecklare skapa bibliotek som riktar sig mot vanliga API:er som delas av .NET Framework och .NET 5 (och .NET Core) och senare versioner. Utvecklare förväntar sig att ett bibliotek som används i ett .NET 5-program ska bete sig identiskt med samma bibliotek som används i ett .NET Framework-program.
Tillsammans med kompatibilitet mellan .NET-implementeringar förväntar sig utvecklare en hög kompatibilitetsnivå mellan versioner av en viss implementering av .NET. I synnerhet bör kod som skrivits för en tidigare version av .NET Core köras sömlöst på .NET 5 eller en senare version. Faktum är att många utvecklare förväntar sig att de nya API:erna som finns i nyligen släppta versioner av .NET också ska vara kompatibla med förhandsversionerna där dessa API:er introducerades.
Den här artikeln beskriver ändringar som påverkar kompatibiliteten och hur .NET-teamet utvärderar varje typ av ändring. Att förstå hur .NET-teamet närmar sig möjliga icke-bakåtkompatibla ändringar är särskilt användbart för utvecklare som öppnar pull-begäranden som ändrar beteendet för befintliga .NET-API:er.
I följande avsnitt beskrivs kategorierna med ändringar som gjorts i .NET-API:er och deras inverkan på programkompatibiliteten. Ändringar tillåts antingen (✔️), tillåts inte (❌) eller kräver bedömning och en utvärdering av hur förutsägbart, uppenbart och konsekvent det tidigare beteendet var (❓).
Kommentar
- Förutom att fungera som en guide till hur ändringar i .NET-bibliotek utvärderas, kan biblioteksutvecklare också använda dessa kriterier för att utvärdera ändringar i sina bibliotek som riktar sig mot flera .NET-implementeringar och versioner.
- Information om kompatibilitetskategorierna, till exempel kompatibilitet framåt och bakåt, finns i Hur kodändringar kan påverka kompatibiliteten.
Ändringar i det offentliga kontraktet
Ändringar i den här kategorin ändrar den offentliga ytan av en typ. De flesta av ändringarna i den här kategorin tillåts inte eftersom de bryter mot bakåtkompatibiliteten (möjligheten för ett program som har utvecklats med en tidigare version av ett API att köras utan omkompilering på en senare version).
Typer
✔️ TILLÅTEN: Ta bort en gränssnittsimplementering från en typ när gränssnittet redan har implementerats av en bastyp
❓ KRÄVER BEDÖMNING: Lägga till en ny gränssnittsimplementering till en typ
Detta är en acceptabel ändring eftersom den inte påverkar befintliga klienter negativt. Alla ändringar av typen måste fungera inom gränserna för acceptabla ändringar som definieras här för att den nya implementeringen ska förbli acceptabel. Extrem försiktighet är nödvändig när du lägger till gränssnitt som direkt påverkar möjligheten för en designer eller serialiserare att generera kod eller data som inte kan användas nedåt. Ett exempel är gränssnittet ISerializable .
❓ KRÄVER BEDÖMNING: Introduktion till en ny basklass
En typ kan introduceras i en hierarki mellan två befintliga typer om den inte introducerar några nya abstrakta medlemmar eller ändrar semantiken eller beteendet för befintliga typer. I .NET Framework 2.0 DbConnection blev klassen till exempel en ny basklass för SqlConnection, som tidigare hade härletts direkt från Component.
✔️ TILLÅTET: Flytta en typ från en sammansättning till en annan
Den gamla sammansättningen måste markeras med den TypeForwardedToAttribute som pekar på den nya sammansättningen.
✔️ TILLÅTET: Ändra en structtyp till en
readonly struct
typDet är inte tillåtet att ändra en
readonly struct
typ till enstruct
typ.✔️ TILLÅTEN: Lägga till det förseglade eller abstrakta nyckelordet till en typ när det inte finns några tillgängliga (offentliga eller skyddade) konstruktorer
✔️ TILLÅTEN: Utöka synligheten för en typ
❌OTILLÅTEN: Ändra namnområdet eller namnet på en typ
❌OTILLÅTEN: Byta namn på eller ta bort en offentlig typ
Detta bryter all kod som använder den omdöpta eller borttagna typen.
Kommentar
I sällsynta fall kan .NET ta bort ett offentligt API. Mer information finns i API-borttagning i .NET. För information om . NET:s supportprincip finns i .NET-supportprincip.
❌OTILLÅTEN: Ändra den underliggande typen av en uppräkning
Det här är en kompileringstids- och beteendebrytande ändring samt en binär icke-bakåtkompatibel ändring som kan göra attributargumenten oparsbara.
❌OTILLÅTEN: Försegla en typ som tidigare var oförseglade
❌OTILLÅTEN: Lägga till ett gränssnitt i uppsättningen med bastyper i ett gränssnitt
Om ett gränssnitt implementerar ett gränssnitt som det tidigare inte implementerade, bryts alla typer som implementerade den ursprungliga versionen av gränssnittet.
❓ KRÄVER BEDÖMNING: Ta bort en klass från uppsättningen med basklasser eller ett gränssnitt från uppsättningen implementerade gränssnitt
Det finns ett undantag från regeln för borttagning av gränssnitt: du kan lägga till implementeringen av ett gränssnitt som härleds från det borttagna gränssnittet. Du kan till exempel ta bort IDisposable om typen eller gränssnittet nu implementerar IComponent, som implementerar IDisposable.
❌OTILLÅTEN: Ändra en
readonly struct
typ till en structtypÄndring av en
struct
typ till enreadonly struct
typ tillåts dock.❌OTILLÅTEN: Ändra en structtyp till en
ref struct
typ och vice versa❌OTILLÅTEN: Minska synligheten för en typ
Det är dock tillåtet att öka synligheten för en typ.
Medlemmar
✔️ TILLÅTEN: Utöka synligheten för en medlem som inte är virtuell
✔️ TILLÅTEN: Lägga till en abstrakt medlem i en offentlig typ som inte har några tillgängliga (offentliga eller skyddade) konstruktorer, eller så är typen förseglad
Att lägga till en abstrakt medlem i en typ som har tillgängliga (offentliga eller skyddade) konstruktorer och inte
sealed
tillåts.✔️ TILLÅTEN: Begränsa synligheten för en skyddad medlem när typen inte har några tillgängliga (offentliga eller skyddade) konstruktorer, eller om typen är förseglad
✔️ TILLÅTET: Flytta en medlem till en klass som är högre i hierarkin än den typ som den togs bort från
✔️ TILLÅTEN: Lägga till eller ta bort en åsidosättning
Att införa en åsidosättning kan leda till att tidigare konsumenter hoppar över åsidosättningen när de anropar basen.
✔️ TILLÅTEN: Lägga till en konstruktor i en klass, tillsammans med en parameterlös konstruktor om klassen tidigare inte hade några konstruktorer
Det är dock inte tillåtet att lägga till en konstruktor i en klass som tidigare inte hade några konstruktorer utan att lägga till den parameterlösa konstruktorn.
✔️ TILLÅTET: Ändra från ett
ref readonly
till ettref
returvärde (förutom virtuella metoder eller gränssnitt)✔️ TILLÅTEN: Ta bort skrivskyddat från ett fält, såvida inte den statiska typen av fältet är en föränderlig värdetyp
✔️ TILLÅTEN: Anropa en ny händelse som inte har definierats tidigare
❓ KRÄVER BEDÖMNING: Lägga till ett nytt instansfält till en typ
Den här ändringen påverkar serialiseringen.
❌OTILLÅTEN: Byta namn på eller ta bort en offentlig medlem eller parameter
Detta bryter all kod som använder den omdöpta eller borttagna medlemmen eller parametern.
Detta inkluderar att ta bort eller byta namn på en getter eller setter från en egenskap, samt att byta namn på eller ta bort uppräkningsmedlemmar.
❌OTILLÅTEN: Lägga till en medlem i ett gränssnitt
Om du tillhandahåller en implementering resulterar det inte nödvändigtvis i kompileringsfel i underordnade sammansättningar om du lägger till en ny medlem i ett befintligt gränssnitt. Alla språk har dock inte stöd för standardgränssnittsmedlemmar (DIM). I vissa scenarier kan körningen inte heller bestämma vilken standardgränssnittsmedlem som ska anropas. Därför anses det vara en icke-bakåtkompatibel ändring att lägga till en medlem i ett befintligt gränssnitt.
❌OTILLÅTEN: Ändra värdet för en offentlig konstant eller uppräkningsmedlem
❌OTILLÅTEN: Ändra typen av egenskap, fält, parameter eller returvärde
❌OTILLÅTEN: Lägga till, ta bort eller ändra ordningen på parametrar
❌OTILLÅTEN: Lägga till eller ta bort in-, ut- eller referensnyckelordet från en parameter
❌OTILLÅTEN: Byta namn på en parameter (inklusive att ändra dess skiftläge)
Detta anses vara icke-bakåtkompatibelt av två skäl:
Den bryter scenarier med sen bindning, till exempel funktionen för sen bindning i Visual Basic och dynamisk i C#.
Den bryter källkompatibiliteten när utvecklare använder namngivna argument.
❌OTILLÅTEN: Ändra från ett
ref
returvärde till ettref readonly
returvärde❌️ OTILLÅTEN: Ändra från ett
ref readonly
till ettref
returvärde på en virtuell metod eller ett virtuellt gränssnitt❌OTILLÅTEN: Lägga till eller ta bort abstrakt från en medlem
❌OTILLÅTEN: Ta bort det virtuella nyckelordet från en medlem
❌OTILLÅTEN: Lägga till det virtuella nyckelordet till en medlem
Även om detta ofta inte är en icke-bakåtkompilerare eftersom C#-kompilatorn tenderar att avge instruktioner för callvirt Intermediate Language (IL) för att anropa icke-virtuella metoder (
callvirt
utför en null-kontroll, medan ett normalt anrop inte gör det), är det här beteendet inte alltid av flera skäl:C# är inte det enda språk som .NET riktar in sig på.
C#-kompilatorn försöker i allt högre grad optimera
callvirt
till ett normalt anrop när målmetoden inte är virtuell och förmodligen inte är null (till exempel en metod som nås via operatorn för null-spridning av ?. null).
Att skapa en virtuell metod innebär att konsumentkoden ofta anropar den icke-virtuellt.
❌OTILLÅTEN: Göra en virtuell medlem abstrakt
En virtuell medlem tillhandahåller en metodimplementering som kan åsidosättas av en härledd klass. En abstrakt medlem tillhandahåller ingen implementering och måste åsidosättas.
❌OTILLÅTEN: Lägga till det förseglade nyckelordet till en gränssnittsmedlem
Om du lägger
sealed
till en standardmedlem i gränssnittet blir det icke-virtuellt, vilket förhindrar att en härledd typs implementering av medlemmen anropas.❌OTILLÅTEN: Lägga till en abstrakt medlem i en offentlig typ som har tillgängliga (offentliga eller skyddade) konstruktorer och som inte är förseglade
❌OTILLÅTEN: Lägga till eller ta bort det statiska nyckelordet från en medlem
❌OTILLÅTEN: Lägga till en överlagring som utesluter en befintlig överlagring och definierar ett annat beteende
Detta bryter befintliga klienter som var bundna till den tidigare överlagringen. Om en klass till exempel har en enda version av en metod som accepterar en UInt32, binder en befintlig konsument till överbelastningen när ett Int32 värde skickas. Men om du lägger till en överlagring som accepterar en Int32, vid omkompilering eller användning av sen bindning, binder kompilatorn nu till den nya överbelastningen. Om ett annat beteende resulterar är detta en icke-bakåtkompatibel ändring.
❌OTILLÅTEN: Lägga till en konstruktor i en klass som tidigare inte hade någon konstruktor utan att lägga till den parameterlösa konstruktorn
❌️ OTILLÅTEN: Lägga till skrivskyddat i ett fält
❌OTILLÅTEN: Minska synligheten för en medlem
Detta inkluderar att minska synligheten för en skyddad medlem när det finns tillgängliga (
public
ellerprotected
) konstruktorer och typen inte är förseglad. Om så inte är fallet är det tillåtet att minska synligheten för en skyddad medlem.Det är tillåtet att öka synligheten för en medlem.
❌OTILLÅTEN: Ändra typen av medlem
Det går inte att ändra returvärdet för en metod eller typen av en egenskap eller ett fält. Signaturen för en metod som returnerar en Object kan till exempel inte ändras för att returnera en String, eller tvärtom.
❌OTILLÅTEN: Lägga till ett instansfält i en struct som inte har några icke-offentliga fält
Om en struct bara har offentliga fält eller inte har några fält alls kan anropare deklarera lokalbefolkningen av den structtypen utan att anropa struct-konstruktorn eller först initiera den lokala till
default(T)
, så länge alla offentliga fält har angetts på struct före första användning. Att lägga till nya fält – offentliga eller icke-offentliga – i en sådan struct är en källbrytande ändring för dessa anropare, eftersom kompilatorn nu kräver att de ytterligare fälten initieras.Att lägga till nya fält – offentliga eller icke-offentliga – i en struct utan fält eller endast offentliga fält är dessutom en binär icke-bakåtkompatibel ändring för anropare som har tillämpat på
[SkipLocalsInit]
koden. Eftersom kompilatorn inte var medveten om dessa fält vid kompileringstillfället kan den generera IL som inte helt initierar structen, vilket leder till att struct skapas från ennitialiserade stackdata.Om en struct har några icke-offentliga fält framtvingar kompilatorn redan initiering via konstruktorn eller
default(T)
, och att lägga till nya instansfält är inte en icke-bakåtkompatibel ändring.❌OTILLÅTEN: Utlös en befintlig händelse när den aldrig utlöstes tidigare
Förändringar i beteende
Sammansättningar
✔️ TILLÅTEN: Gör en sammansättning portabel när samma plattformar fortfarande stöds
❌OTILLÅTEN: Ändra namnet på en sammansättning
❌OTILLÅTEN: Ändra den offentliga nyckeln för en sammansättning
Egenskaper, fält, parametrar och returvärden
✔️ TILLÅTET: Ändra värdet för en egenskap, ett fält, ett returvärde eller en ut-parameter till en mer härledd typ
En metod som returnerar en typ av Object kan till exempel returnera en String instans. (Metodens signatur kan dock inte ändras.)
✔️ TILLÅTEN: Öka intervallet med godkända värden för en egenskap eller parameter om medlemmen inte är virtuell
Även om det intervall med värden som kan skickas till metoden eller returneras av medlemmen kan expandera, kan parametern eller medlemstypen inte göra det. Även om värdena som skickas till en metod kan utökas från 0–124 till 0–255 kan parametertypen inte ändras från Byte till Int32.
❌OTILLÅTEN: Öka intervallet med godkända värden för en egenskap eller parameter om medlemmen är virtuell
Den här ändringen bryter befintliga åsidosatta medlemmar, som inte fungerar korrekt för det utökade intervallet med värden.
❌OTILLÅTEN: Minska intervallet för godkända värden för en egenskap eller parameter
❌OTILLÅTEN: Öka intervallet för returnerade värden för en egenskap, ett fält, ett returvärde eller en out-parameter
❌OTILLÅTEN: Ändra returnerade värden för en egenskap, ett fält, ett metodreturvärde eller en out-parameter
❌OTILLÅTEN: Ändra standardvärdet för en egenskap, ett fält eller en parameter
Att ändra eller ta bort ett standardvärde för parametern är inte en binär brytning. Att ta bort ett standardvärde för parametern är en källbrytning, och om du ändrar ett standardvärde för parametern kan det leda till en beteendeavbrott efter omkompilering.
Därför är det acceptabelt att ta bort standardvärden för parametern i det specifika fallet att "flytta" dessa standardvärden till en ny metodöverlagring för att eliminera tvetydighet. Du kan till exempel överväga en befintlig metod
MyMethod(int a = 1)
. Om du introducerar en överlagring avMyMethod
med två valfria parametrara
ochb
kan du bevara kompatibiliteten genom att flytta standardvärdeta
för till den nya överlagringen. Nu ärMyMethod(int a)
de två överlagringarna ochMyMethod(int a = 1, int b = 2)
. Det här mönstret gör det möjligtMyMethod()
att kompilera.❌OTILLÅTEN: Ändra precisionen för ett numeriskt returvärde
❓ KRÄVER BEDÖMNING: En ändring i parsningen av indata och utlöser nya undantag (även om parsningsbeteendet inte anges i dokumentationen
Undantag
✔️ TILLÅTEN: Utlöser ett mer härlett undantag än ett befintligt undantag
Eftersom det nya undantaget är en underklass av ett befintligt undantag fortsätter den tidigare undantagshanteringskoden att hantera undantaget. I .NET Framework 4 började till exempel metoder för kulturskapande och hämtning att skapa en CultureNotFoundException i stället för en ArgumentException om kulturen inte kunde hittas. Eftersom CultureNotFoundException härleds från ArgumentExceptionär detta en acceptabel ändring.
✔️ TILLÅTEN: Utlöser ett mer specifikt undantag än NotSupportedException, NotImplementedException, NullReferenceException
✔️ TILLÅTEN: Utlöser ett undantag som anses vara oåterkalleligt
Oåterkalleliga undantag bör inte fångas utan i stället hanteras av en övergripande catch-all-hanterare. Därför förväntas användarna inte ha kod som fångar upp dessa explicita undantag. De oåterkalleliga undantagen är:
✔️ TILLÅTEN: Utlöser ett nytt undantag i en ny kodsökväg
Undantaget får endast gälla för en ny kodsökväg som körs med nya parametervärden eller tillstånd och som inte kan köras av befintlig kod som riktar sig mot den tidigare versionen.
✔️ TILLÅTEN: Ta bort ett undantag för att aktivera mer robust beteende eller nya scenarier
En metod som tidigare endast hanterade positiva värden och kastade ett ArgumentOutOfRangeException annat kan till exempel
Divide
ändras för att stödja både negativa och positiva värden utan att utlösa ett undantag.✔️ TILLÅTEN: Ändra texten i ett felmeddelande
Utvecklare bör inte förlita sig på texten i felmeddelanden, som också ändras baserat på användarens kultur.
❌OTILLÅTEN: Utlöser ett undantag i andra fall som inte anges ovan
❌OTILLÅTEN: Ta bort ett undantag i andra fall som inte anges ovan
Attribut
✔️ TILLÅTEN: Ändra värdet för ett attribut som inte kan observeras
❌OTILLÅTEN: Ändra värdet för ett attribut som kan observeras
❓ KRÄVER BEDÖMNING: Ta bort ett attribut
I de flesta fall är det en icke-bakåtkompatibel ändring att ta bort ett attribut (till exempel NonSerializedAttribute) .
Plattformssupport
✔️ TILLÅTEN: Stöd för en åtgärd på en plattform som tidigare inte stöds
❌OTILLÅTEN: Stöder inte eller kräver nu ett specifikt servicepaket för en åtgärd som tidigare stöddes på en plattform
Interna implementeringsändringar
❓ KRÄVER BEDÖMNING: Ändra ytan av en intern typ
Sådana ändringar är i allmänhet tillåtna, även om de bryter privat reflektion. I vissa fall, där populära bibliotek från tredje part eller ett stort antal utvecklare är beroende av interna API:er, är sådana ändringar kanske inte tillåtna.
❓ KRÄVER DOM: Ändra den interna implementeringen av en medlem
Dessa ändringar är vanligtvis tillåtna, även om de bryter privat reflektion. I vissa fall, där kundkod ofta är beroende av privat reflektion eller där ändringen medför oavsiktliga biverkningar, är dessa ändringar kanske inte tillåtna.
✔️ TILLÅTET: Förbättra prestanda för en åtgärd
Möjligheten att ändra prestanda för en åtgärd är viktig, men sådana ändringar kan bryta kod som förlitar sig på den aktuella hastigheten för en åtgärd. Detta gäller särskilt för kod som beror på tidpunkten för asynkrona åtgärder. Prestandaändringen bör inte ha någon effekt på det aktuella API:ets andra beteende. annars kommer ändringen att brytas.
✔️ TILLÅTET: Indirekt (och ofta negativt) ändra prestanda för en åtgärd
Om ändringen i fråga inte kategoriseras som icke-bakåtkompatibel av någon annan anledning är detta acceptabelt. Ofta måste åtgärder vidtas som kan omfatta extra åtgärder eller som lägger till nya funktioner. Detta påverkar nästan alltid prestanda, men kan vara viktigt för att API:et i fråga ska fungera som förväntat.
❌OTILLÅTEN: Ändra ett synkront API till asynkront (och vice versa)
Kodändringar
✔️ TILLÅTEN: Lägga till params i en parameter
❌OTILLÅTEN: Lägga till det markerade nyckelordet i ett kodblock
Den här ändringen kan orsaka att kod som tidigare kördes genererar en OverflowException och är oacceptabel.
❌OTILLÅTEN: Ta bort params från en parameter
❌OTILLÅTEN: Ändra i vilken ordning händelser utlöses
Utvecklare kan rimligen förvänta sig att händelser utlöses i samma ordning, och utvecklarkod beror ofta på i vilken ordning händelser utlöses.
❌OTILLÅTEN: Ta bort höjningen av en händelse för en viss åtgärd
❌OTILLÅTEN: Ändra antalet gånger som angivna händelser anropas
❌OTILLÅTEN: Lägga till i FlagsAttribute en uppräkningstyp