Dela via


Serialisering och deserialisering

Windows Communication Foundation (WCF) innehåller en ny serialiseringsmotor, DataContractSerializer. Översätter DataContractSerializer mellan .NET Framework-objekt och XML i båda riktningarna. I det här avsnittet beskrivs hur serialiseraren fungerar.

När .NET Framework-objekt serialiseras förstår serialiseraren en mängd olika serialiseringsprogrammeringsmodeller, inklusive den nya datakontraktsmodellen . En fullständig lista över typer som stöds finns i Typer som stöds av Data Contract Serializer. En introduktion till datakontrakt finns i Använda datakontrakt.

Vid deserialisering av XML använder serialiseraren klasserna XmlReader och XmlWriter . Den stöder också klasserna XmlDictionaryReader och XmlDictionaryWriter för att göra det möjligt för den att skapa optimerad XML i vissa fall, till exempel när du använder det binära XML-formatet WCF.

WCF innehåller också en tillhörande serialiserare, NetDataContractSerializer. NetDataContractSerializer:

  • Är inte säker. Mer information finns i säkerhetsguiden för BinaryFormatter.
  • Liknar serialiserarna BinaryFormatter och SoapFormatter eftersom den också genererar .NET Framework-typnamn som en del av serialiserade data.
  • Används när samma typer delas på serialiseringen och deserialiseringen slutar.

Både DataContractSerializer och NetDataContractSerializer härleds från en gemensam basklass, XmlObjectSerializer.

Varning

Serialiserar DataContractSerializer strängar som innehåller kontrolltecken med ett hexadecimalt värde under 20 som XML-entiteter. Detta kan orsaka problem med en icke-WCF-klient när sådana data skickas till en WCF-tjänst.

Skapa en DataContractSerializer-instans

Att konstruera en instans av DataContractSerializer är ett viktigt steg. Efter konstruktionen kan du inte ändra någon av inställningarna.

Ange rottyp

Rottypen är den typ av instanser som serialiseras eller deserialiseras. Har DataContractSerializer många konstruktoröverlagringar, men minst måste en rottyp anges med parametern type .

En serialiserare som skapats för en viss rottyp kan inte användas för att serialisera (eller deserialisera) en annan typ, såvida inte typen härleds från rottypen. I följande exempel visas två klasser.

[DataContract]
public class Person
{
    // Code not shown.
}

[DataContract]
public class PurchaseOrder
{
    // Code not shown.
}
<DataContract()> _
Public Class Person
    ' Code not shown.
End Class

<DataContract()> _
Public Class PurchaseOrder
    ' Code not shown.
End Class

Den här koden konstruerar en instans av DataContractSerializer som endast kan användas för att serialisera eller deserialisera instanser av Person klassen.

DataContractSerializer dcs = new DataContractSerializer(typeof(Person));
// This can now be used to serialize/deserialize Person but not PurchaseOrder.
Dim dcs As New DataContractSerializer(GetType(Person))
' This can now be used to serialize/deserialize Person but not PurchaseOrder.

Ange kända typer

Om polymorfism ingår i de typer som serialiseras som inte redan hanteras med hjälp av KnownTypeAttribute attributet eller någon annan mekanism, måste en lista över möjliga kända typer skickas till serialiserarens konstruktor med hjälp av parametern knownTypes . Mer information om kända typer finns i Kända typer av datakontrakt.

I följande exempel visas en klass, LibraryPatron, som innehåller en samling av en viss typ, LibraryItem. Den andra klassen definierar LibraryItem typen. Den tredje och fyra klasserna (Book och Newspaper) ärver från LibraryItem klassen.

[DataContract]
public class LibraryPatron
{
    [DataMember]
    public LibraryItem[] borrowedItems;
}
[DataContract]
public class LibraryItem
{
    // Code not shown.
}

[DataContract]
public class Book : LibraryItem
{
    // Code not shown.
}

[DataContract]
public class Newspaper : LibraryItem
{
    // Code not shown.
}
<DataContract()> _
Public Class LibraryPatron
    <DataMember()> _
    Public borrowedItems() As LibraryItem
End Class

<DataContract()> _
Public Class LibraryItem
    ' Code not shown.
End Class

<DataContract()> _
Public Class Book
    Inherits LibraryItem
    ' Code not shown.
End Class

<DataContract()> _
Public Class Newspaper
    Inherits LibraryItem
    ' Code not shown.
End Class

Följande kod konstruerar en instans av serialiseraren med hjälp av parametern knownTypes .

// Create a serializer for the inherited types using the knownType parameter.
Type[] knownTypes = new Type[] { typeof(Book), typeof(Newspaper) };
DataContractSerializer dcs =
new DataContractSerializer(typeof(LibraryPatron), knownTypes);
// All types are known after construction.
' Create a serializer for the inherited types using the knownType parameter.
Dim knownTypes() As Type = {GetType(Book), GetType(Newspaper)}
Dim dcs As New DataContractSerializer(GetType(LibraryPatron), knownTypes)
' All types are known after construction.

Ange standardrotnamn och namnområde

När ett objekt serialiseras bestäms normalt standardnamnet och namnområdet för det yttersta XML-elementet enligt datakontraktets namn och namnområde. Namnen på alla inre element bestäms från datamedlemsnamn och deras namnområde är datakontraktets namnområde. Följande exempeluppsättningar Name och Namespace värden i konstruktorerna i klasserna DataContractAttribute och DataMemberAttribute .

[DataContract(Name = "PersonContract", Namespace = "http://schemas.contoso.com")]
public class Person2
{
    [DataMember(Name = "AddressMember")]
    public Address theAddress;
}

[DataContract(Name = "AddressContract", Namespace = "http://schemas.contoso.com")]
public class Address
{
    [DataMember(Name = "StreetMember")]
    public string street;
}
<DataContract(Name:="PersonContract", [Namespace]:="http://schemas.contoso.com")> _
Public Class Person2
    <DataMember(Name:="AddressMember")> _
    Public theAddress As Address
End Class

<DataContract(Name:="AddressContract", [Namespace]:="http://schemas.contoso.com")> _
Public Class Address
    <DataMember(Name:="StreetMember")> _
    Public street As String
End Class

Serialisering av en instans av Person klassen ger XML som liknar följande.

<PersonContract xmlns="http://schemas.contoso.com">  
  <AddressMember>  
    <StreetMember>123 Main Street</StreetMember>  
   </AddressMember>  
</PersonContract>  

Du kan dock anpassa standardnamnet och namnområdet för rotelementet genom att skicka värdena för parametrarna rootName och rootNamespace till DataContractSerializer konstruktorn. Observera att rootNamespace inte påverkar namnområdet för de inneslutna element som motsvarar datamedlemmar. Det påverkar endast namnområdet för det yttersta elementet.

Dessa värden kan skickas som strängar eller instanser av klassen för att tillåta deras optimering med hjälp av XmlDictionaryString det binära XML-formatet.

Ange maximal kvot för objekt

Vissa DataContractSerializer konstruktoröverlagringar har en maxItemsInObjectGraph parameter. Den här parametern avgör det maximala antalet objekt som serialiseraren serialiserar eller deserialiserar i ett enda ReadObject metodanrop. (Metoden läser alltid ett rotobjekt, men det här objektet kan ha andra objekt i sina datamedlemmar. Objekten kan ha andra objekt och så vidare.) Standardvärdet är 65536. Observera att när matriser serialiseras eller deserialiseras räknas varje matrispost som ett separat objekt. Observera också att vissa objekt kan ha en stor minnesrepresentation, så enbart den här kvoten kanske inte räcker för att förhindra en överbelastningsattack. Mer information finns i Säkerhetsöverväganden för data. Om du behöver öka den här kvoten utöver standardvärdet är det viktigt att göra det både på sidorna för att skicka (serialisera) och ta emot (deserialisera) eftersom det gäller både vid läsning och skrivning av data.

Tur och retur

En rundresa inträffar när ett objekt deserialiseras och serialiseras på nytt i en åtgärd. Det innebär att den går från XML till en objektinstans och tillbaka igen till en XML-dataström.

Vissa DataContractSerializer konstruktoröverlagringar har en ignoreExtensionDataObject parameter som är inställd false på som standard. I det här standardläget kan data skickas tur och retur från en nyare version av ett datakontrakt via en äldre version och tillbaka till den nyare versionen utan förlust, så länge datakontraktet IExtensibleDataObject implementerar gränssnittet. Anta till exempel att version 1 av datakontraktet Person innehåller Name datamedlemmarna och PhoneNumber och version 2 lägger till en Nickname medlem. Om IExtensibleDataObject implementeras, när du skickar information från version 2 till version 1, Nickname lagras data och genereras sedan igen när data serialiseras igen. Därför går inga data förlorade under tur och retur. Mer information finns i Framåtkompatibla datakontrakt och Versionshantering av datakontrakt.

Problem med säkerhet och schema giltighet med tur och retur

Tur och retur kan ha säkerhetskonsekvenser. Till exempel kan det vara en säkerhetsrisk att deserialisera och lagra stora mängder onödiga data. Det kan finnas säkerhetsproblem vid återutsändande av dessa data som det inte finns något sätt att verifiera, särskilt om digitala signaturer är inblandade. I det föregående scenariot kan till exempel version 1-slutpunkten signera ett Nickname värde som innehåller skadliga data. Slutligen kan det finnas problem med schemats giltighet: en slutpunkt kanske alltid vill generera data som strikt följer det angivna kontraktet och inte några extra värden. I föregående exempel säger version 1-slutpunktens kontrakt att det endast Name genererar och PhoneNumber, och om schemavalidering används genererar det extra Nickname värdet att valideringen misslyckas.

Aktivera och inaktivera rundresor

Om du vill inaktivera rundresor implementerar IExtensibleDataObject du inte gränssnittet. Om du inte har någon kontroll över typerna anger du parametern ignoreExtensionDataObject till true för att uppnå samma effekt.

Bevarande av objektdiagram

Normalt bryr sig serialiseraren inte om objektidentitet, som i följande kod.

[DataContract]
public class PurchaseOrder
{
    [DataMember]
    public Address billTo;
    [DataMember]
    public Address shipTo;
}

[DataContract]
public class Address
{
    [DataMember]
    public string street;
}
<DataContract()> _
Public Class PurchaseOrder

    <DataMember()> _
    Public billTo As Address

    <DataMember()> _
    Public shipTo As Address

End Class

<DataContract()> _
Public Class Address

    <DataMember()> _
    Public street As String

End Class

Följande kod skapar en inköpsorder.

// Construct a purchase order:
Address adr = new Address();
adr.street = "123 Main St.";
PurchaseOrder po = new PurchaseOrder();
po.billTo = adr;
po.shipTo = adr;
' Construct a purchase order:
Dim adr As New Address()
adr.street = "123 Main St."
Dim po As New PurchaseOrder()
po.billTo = adr
po.shipTo = adr

Observera att billTo fälten och shipTo är inställda på samma objektinstans. Den genererade XML-koden duplicerar dock den duplicerade informationen och ser ut ungefär som följande XML.

<PurchaseOrder>  
  <billTo><street>123 Main St.</street></billTo>  
  <shipTo><street>123 Main St.</street></shipTo>  
</PurchaseOrder>  

Den här metoden har dock följande egenskaper, som kan vara oönskade:

  • Prestanda. Det är ineffektivt att replikera data.

  • Cirkelreferenser. Om objekt refererar till sig själva, även via andra objekt, resulterar serialisering efter replikering i en oändlig loop. (Serialiseraren genererar en SerializationException om detta händer.)

  • Semantics. Ibland är det viktigt att bevara det faktum att två referenser är till samma objekt och inte till två identiska objekt.

Av dessa skäl har vissa DataContractSerializer konstruktoröverlagringar en preserveObjectReferences parameter (standardvärdet är false). När den här parametern är inställd truepå används en särskild metod för kodning av objektreferenser, som endast WCF förstår. När xml-kodexemplet är inställt truepå liknar det nu följande.

<PurchaseOrder ser:id="1">  
  <billTo ser:id="2"><street ser:id="3">123 Main St.</street></billTo>  
  <shipTo ser:ref="2"/>  
</PurchaseOrder>  

Namnområdet "ser" refererar till standardnamnområdet för serialisering, http://schemas.microsoft.com/2003/10/Serialization/. Varje del av data serialiseras bara en gång och ges ett ID-nummer, och efterföljande användning resulterar i en referens till redan serialiserade data.

Viktigt!

Om både attributen "id" och "ref" finns i datakontraktet XMLElementrespekteras attributet "ref" och attributet "id" ignoreras.

Det är viktigt att förstå begränsningarna i det här läget:

  • XML-koden som DataContractSerializer skapas med preserveObjectReferences inställd på true är inte kompatibel med andra tekniker och kan endast nås av en annan DataContractSerializer instans, även med preserveObjectReferences inställd på true.

  • Det finns inget stöd för metadata (schema) för den här funktionen. Schemat som skapas är endast giltigt för fallet när preserveObjectReferences är inställt på false.

  • Den här funktionen kan göra att serialiserings- och deserialiseringsprocessen körs långsammare. Även om data inte behöver replikeras måste extra objektjämförelser utföras i det här läget.

Varning

preserveObjectReferences När läget är aktiverat är det särskilt viktigt att ange maxItemsInObjectGraph värdet till rätt kvot. På grund av hur matriser hanteras i det här läget är det enkelt för en angripare att skapa ett litet skadligt meddelande som resulterar i stor minnesförbrukning som endast begränsas av maxItemsInObjectGraph kvoten.

Ange ett datakontraktssurrogat

Vissa DataContractSerializer konstruktoröverlagringar har en dataContractSurrogate parameter som kan vara inställd på null. Annars kan du använda den för att ange en surrogat för datakontrakt, vilket är en typ som implementerar IDataContractSurrogate gränssnittet. Du kan sedan använda gränssnittet för att anpassa serialiserings- och deserialiseringsprocessen. Mer information finns i Surrogater för datakontrakt.

Serialization

Följande information gäller för alla klasser som ärver från XmlObjectSerializer, inklusive klasserna DataContractSerializer och NetDataContractSerializer .

Enkel serialisering

Det mest grundläggande sättet att serialisera ett objekt är att skicka det till WriteObject metoden. Det finns tre överlagringar, en vardera för att skriva till en Stream, en XmlWritereller en XmlDictionaryWriter. Med överlagringen Stream är utdata XML i UTF-8-kodningen. Med överlagringen XmlDictionaryWriter optimerar serialiseraren sina utdata för binär XML.

När du använder WriteObject metoden använder serialiseraren standardnamnet och namnområdet för omslutningselementet och skriver ut det tillsammans med innehållet (se föregående avsnitt "Ange standardrotnamn och namnområde").

I följande exempel visas hur du skriver med en XmlDictionaryWriter.

Person p = new Person();
DataContractSerializer dcs =
    new DataContractSerializer(typeof(Person));
XmlDictionaryWriter xdw =
    XmlDictionaryWriter.CreateTextWriter(someStream,Encoding.UTF8 );
dcs.WriteObject(xdw, p);
Dim p As New Person()
Dim dcs As New DataContractSerializer(GetType(Person))
Dim xdw As XmlDictionaryWriter = _
    XmlDictionaryWriter.CreateTextWriter(someStream, Encoding.UTF8)
dcs.WriteObject(xdw, p)

Detta genererar XML som liknar följande.

<Person>  
  <Name>Jay Hamlin</Name>  
  <Address>123 Main St.</Address>  
</Person>  

Stegvis serialisering

WriteStartObjectAnvänd metoderna , WriteObjectContentoch WriteEndObject för att skriva slutelementet, skriva objektinnehållet och stänga omslutningselementet.

Kommentar

Det finns inga Stream överlagringar av dessa metoder.

Den här stegvisa serialiseringen har två vanliga användningsområden. Det ena är att infoga innehåll som attribut eller kommentarer mellan WriteStartObject och WriteObjectContent, som du ser i följande exempel.

dcs.WriteStartObject(xdw, p);
xdw.WriteAttributeString("serializedBy", "myCode");
dcs.WriteObjectContent(xdw, p);
dcs.WriteEndObject(xdw);
dcs.WriteStartObject(xdw, p)
xdw.WriteAttributeString("serializedBy", "myCode")
dcs.WriteObjectContent(xdw, p)
dcs.WriteEndObject(xdw)

Detta genererar XML som liknar följande.

<Person serializedBy="myCode">  
  <Name>Jay Hamlin</Name>  
  <Address>123 Main St.</Address>  
</Person>  

En annan vanlig användning är att undvika att använda WriteStartObject och WriteEndObject helt och hållet, och att skriva ett eget anpassat omslutningselement (eller till och med hoppa över att skriva en omslutning helt och hållet), som du ser i följande kod.

xdw.WriteStartElement("MyCustomWrapper");
dcs.WriteObjectContent(xdw, p);
xdw.WriteEndElement();
xdw.WriteStartElement("MyCustomWrapper")
dcs.WriteObjectContent(xdw, p)
xdw.WriteEndElement()

Detta genererar XML som liknar följande.

<MyCustomWrapper>  
  <Name>Jay Hamlin</Name>  
  <Address>123 Main St.</Address>  
</MyCustomWrapper>  

Kommentar

Om du använder stegvis serialisering kan det resultera i schema-ogiltig XML.

Deserialisering

Följande information gäller för alla klasser som ärver från XmlObjectSerializer, inklusive klasserna DataContractSerializer och NetDataContractSerializer .

Det mest grundläggande sättet att deserialisera ett objekt är att anropa en av ReadObject metodöverlagringarna. Det finns tre överlagringar, en var för läsning med en XmlDictionaryReader, en XmlReadereller en Stream. Observera att överlagringen Stream skapar en text som XmlDictionaryReader inte skyddas av några kvoter och endast ska användas för att läsa betrodda data.

Observera också att objektet ReadObject som metoden returnerar måste gjutas till lämplig typ.

Följande kod konstruerar en instans av DataContractSerializer och sedan XmlDictionaryReaderdeserialiserar en Person instans.

DataContractSerializer dcs = new DataContractSerializer(typeof(Person));
FileStream fs = new FileStream(path, FileMode.Open);
XmlDictionaryReader reader =
XmlDictionaryReader.CreateTextReader(fs, new XmlDictionaryReaderQuotas());

Person p = (Person)dcs.ReadObject(reader);
Dim dcs As New DataContractSerializer(GetType(Person))
Dim fs As New FileStream(path, FileMode.Open)
Dim reader As XmlDictionaryReader = _
   XmlDictionaryReader.CreateTextReader(fs, New XmlDictionaryReaderQuotas())

Dim p As Person = CType(dcs.ReadObject(reader), Person)

Innan du anropar ReadObject metoden placerar du XML-läsaren på omslutningselementet eller på en nod som inte är innehåll som föregår omslutningselementet. Du kan göra detta genom att anropa Read metoden XmlReader för eller dess härledning och testa NodeType, som du ser i följande kod.

DataContractSerializer ser = new DataContractSerializer(typeof(Person),
"Customer", @"http://www.contoso.com");
FileStream fs = new FileStream(path, FileMode.Open);
XmlDictionaryReader reader =
XmlDictionaryReader.CreateTextReader(fs, new XmlDictionaryReaderQuotas());
while (reader.Read())
{
    switch (reader.NodeType)
    {
        case XmlNodeType.Element:
            if (ser.IsStartObject(reader))
            {
                Console.WriteLine("Found the element");
                Person p = (Person)ser.ReadObject(reader);
                Console.WriteLine("{0} {1}    id:{2}",
                    p.Name , p.Address);
            }
            Console.WriteLine(reader.Name);
            break;
    }
}
Dim ser As New DataContractSerializer(GetType(Person), "Customer", "http://www.contoso.com")
Dim fs As New FileStream(path, FileMode.Open)
Dim reader As XmlDictionaryReader = XmlDictionaryReader.CreateTextReader(fs, New XmlDictionaryReaderQuotas())

While reader.Read()
    Select Case reader.NodeType
        Case XmlNodeType.Element
            If ser.IsStartObject(reader) Then
                Console.WriteLine("Found the element")
                Dim p As Person = CType(ser.ReadObject(reader), Person)
                Console.WriteLine("{0} {1}", _
                                   p.Name, p.Address)
            End If
            Console.WriteLine(reader.Name)
    End Select
End While

Observera att du kan läsa attribut för det här omslutningselementet innan du ger läsaren till ReadObject.

När du använder en av de enkla ReadObject överlagringarna letar deserialiseraren efter standardnamnet och namnområdet på omslutningselementet (se föregående avsnitt, "Ange standardrotnamn och namnområde") och genererar ett undantag om det hittar ett okänt element. I föregående exempel förväntas omslutningselementet <Person> . Metoden IsStartObject anropas för att verifiera att läsaren är placerad på ett element som heter som förväntat.

Det finns ett sätt att inaktivera namnkontrollen för det här omslutningselementet. vissa överlagringar av ReadObject metoden tar den booleska parametern verifyObjectName, som är inställd true på som standard. När värdet falseär inställt på ignoreras namnet och namnområdet för omslutningselementet. Detta är användbart för att läsa XML som skrevs med hjälp av den stegvisa serialiseringsmekanismen som beskrevs tidigare.

Använda NetDataContractSerializer

Den primära skillnaden mellan DataContractSerializer och är att DataContractSerializer använder namn på datakontrakt, medan NetDataContractSerializer utdata för fullständig .NET Framework-sammansättning och typnamn i NetDataContractSerializer serialiserad XML. Det innebär att exakt samma typer måste delas mellan serialiserings- och deserialiseringsslutpunkterna. Det innebär att mekanismen för kända typer inte krävs med NetDataContractSerializer eftersom de exakta typer som ska deserialiseras alltid är kända.

Flera problem kan dock uppstå:

  • Säkerhet. Alla typer som finns i XML-koden som deserialiseras läses in. Detta kan utnyttjas för att tvinga inläsning av skadliga typer. Användning av NetDataContractSerializer ej betrodda data bör endast göras om en Serialiseringsbindning används (med egenskapen Binder eller konstruktorparametern). Pärmen tillåter endast att säkra typer läses in. Binder-mekanismen är identisk med den som används i System.Runtime.Serialization namnområdet.

  • Versionshantering. Om du använder fullständiga typ- och sammansättningsnamn i XML begränsas allvarligt hur typer kan versionshanteras. Det går inte att ändra följande: typnamn, namnområden, sammansättningsnamn och sammansättningsversioner. AssemblyFormat Om du anger egenskapen eller konstruktorparametern till Simple i stället för standardvärdet för tillåts ändringar i Full sammansättningsversioner, men inte för generiska parametertyper.

  • Samverkan. Eftersom .NET Framework-typ- och sammansättningsnamn ingår i XML kan andra plattformar än .NET Framework inte komma åt resulterande data.

  • Prestanda. Om du skriver ut typ- och sammansättningsnamnen ökar storleken på den resulterande XML:en avsevärt.

Den här mekanismen liknar binär- eller SOAP-serialisering som används av .NET Framework-fjärrkommunikation (specifikt BinaryFormatter och SoapFormatter).

NetDataContractSerializer Att använda liknar att använda DataContractSerializer, med följande skillnader:

  • Konstruktorerna kräver inte att du anger en rottyp. Du kan serialisera vilken typ som helst med samma instans av NetDataContractSerializer.

  • Konstruktorerna accepterar inte en lista över kända typer. Mekanismen för kända typer är onödig om typnamn serialiseras till XML.

  • Konstruktorerna accepterar inte ett datakontrakts surrogat. I stället accepterar de en ISurrogateSelector parameter som heter surrogateSelector (som mappar till egenskapen SurrogateSelector ). Det här är en äldre surrogatmekanism.

  • Konstruktorerna accepterar en parameter som kallas assemblyFormat för den FormatterAssemblyStyle som mappar till egenskapen AssemblyFormat . Som tidigare nämnts kan detta användas för att förbättra serialiserarens versionsfunktioner. Detta är identiskt med mekanismen FormatterAssemblyStyle i binär- eller SOAP-serialisering.

  • Konstruktörerna accepterar en StreamingContext parameter med namnet context som mappar till egenskapen Context . Du kan använda detta för att skicka information till typer som serialiseras. Den här användningen är identisk med den StreamingContext mekanism som används i andra System.Runtime.Serialization klasser.

  • Metoderna Serialize och Deserialize är alias för WriteObject metoderna och ReadObject . Dessa finns för att ge en mer konsekvent programmeringsmodell med binär- eller SOAP-serialisering.

Mer information om dessa funktioner finns i Binär serialisering.

DE XML-format som NetDataContractSerializer används och DataContractSerializer är normalt inte kompatibla. Att försöka serialisera med en av dessa serialiserare och deserialisera med den andra är alltså inte ett scenario som stöds.

Observera också att NetDataContractSerializer inte matar ut den fullständiga .NET Framework-typen och sammansättningsnamnet för varje nod i objektdiagrammet. Den matar ut den informationen endast där den är tvetydig. Det vill:et matas ut på rotobjektnivå och för alla polymorfa fall.

Se även