Dela via


Arv i C# och .NET

I den här självstudien får du lära dig arv i C#. Arv är en funktion i objektorienterade programmeringsspråk som gör att du kan definiera en basklass som ger specifika funktioner (data och beteende) och definiera härledda klasser som antingen ärver eller åsidosätter den funktionen.

Förutsättningar

Köra exemplen

Om du vill skapa och köra exemplen i den här självstudien använder du dotnet-verktyget från kommandoraden. Följ dessa steg för varje exempel:

  1. Skapa en katalog för att lagra exemplet.

  2. Ange det nya konsolkommandot dotnet i en kommandotolk för att skapa ett nytt .NET Core-projekt.

  3. Kopiera och klistra in koden från exemplet i kodredigeraren.

  4. Ange kommandot dotnet restore från kommandoraden för att läsa in eller återställa projektets beroenden.

    Du behöver inte köra dotnet restore eftersom den körs implicit av alla kommandon som kräver en återställning, till exempel dotnet new, dotnet build, dotnet run, dotnet test, dotnet publishoch dotnet pack. Om du vill inaktivera implicit återställning använder du alternativet --no-restore .

    Kommandot dotnet restore är fortfarande användbart i vissa scenarier där det är meningsfullt att uttryckligen återställa, till exempel kontinuerliga integreringsversioner i Azure DevOps Services eller i byggsystem som uttryckligen behöver styra när återställningen sker.

    Information om hur du hanterar NuGet-feeds finns i dokumentationendotnet restore.

  5. Ange kommandot dotnet run för att kompilera och köra exemplet.

Bakgrund: Vad är arv?

Arv är ett av de grundläggande attributen för objektorienterad programmering. Du kan definiera en underordnad klass som återanvänder (ärver), utökar eller ändrar beteendet för en överordnad klass. Klassen vars medlemmar ärvs kallas för basklassen. Klassen som ärver medlemmarna i basklassen kallas för den härledda klassen.

C# och .NET stöder endast enskilt arv . En klass kan alltså bara ärva från en enda klass. Arv är dock transitivt, vilket gör att du kan definiera en arvshierarki för en uppsättning typer. Med andra ord kan typen D ärva från typen C, som ärver från typen B, som ärver från basklasstypen A. Eftersom arv är transitivt är medlemmarna av typen A tillgängliga för att skriva D.

Alla medlemmar i en basklass ärvs inte av härledda klasser. Följande medlemmar ärvs inte:

Alla andra medlemmar i en basklass ärvs av härledda klasser, men om de är synliga eller inte beror på deras tillgänglighet. En medlems tillgänglighet påverkar dess synlighet för härledda klasser på följande sätt:

  • Privata medlemmar visas endast i härledda klasser som är kapslade i sin basklass. Annars visas de inte i härledda klasser. I följande exempel A.B är en kapslad klass som härleds från Aoch C härleds från A. Det privata A._value fältet visas i A.B. Men om du tar bort kommentarerna från C.GetValue metoden och försöker kompilera exemplet genererar den kompilatorfelet CS0122: "'A._value' är otillgänglig på grund av dess skyddsnivå."

    public class A
    {
        private int _value = 10;
    
        public class B : A
        {
            public int GetValue()
            {
                return _value;
            }
        }
    }
    
    public class C : A
    {
        //    public int GetValue()
        //    {
        //        return _value;
        //    }
    }
    
    public class AccessExample
    {
        public static void Main(string[] args)
        {
            var b = new A.B();
            Console.WriteLine(b.GetValue());
        }
    }
    // The example displays the following output:
    //       10
    
  • Skyddade medlemmar visas endast i härledda klasser.

  • Interna medlemmar visas endast i härledda klasser som finns i samma sammansättning som basklassen. De visas inte i härledda klasser som finns i en annan sammansättning än basklassen.

  • Offentliga medlemmar visas i härledda klasser och ingår i den härledda klassens offentliga gränssnitt. Offentliga ärvda medlemmar kan anropas precis som om de definieras i den härledda klassen. I följande exempel definierar klassen A en metod med namnet Method1, och klassen B ärver från klassen A. Exemplet anropar Method1 sedan som om det vore en instansmetod på B.

    public class A
    {
        public void Method1()
        {
            // Method implementation.
        }
    }
    
    public class B : A
    { }
    
    public class Example
    {
        public static void Main()
        {
            B b = new ();
            b.Method1();
        }
    }
    

Härledda klasser kan också åsidosätta ärvda medlemmar genom att tillhandahålla en alternativ implementering. För att kunna åsidosätta en medlem måste medlemmen i basklassen markeras med det virtuella nyckelordet. Som standard markeras inte basklassmedlemmar som virtual och kan inte åsidosättas. Om du försöker åsidosätta en icke-virtuell medlem, som i följande exempel, genereras kompilatorfelet CS0506: "<medlemmen> kan inte åsidosätta ärvd medlemsmedlem <> eftersom den inte har markerats som virtuell, abstrakt eller åsidosättning."

public class A
{
    public void Method1()
    {
        // Do something.
    }
}

public class B : A
{
    public override void Method1() // Generates CS0506.
    {
        // Do something else.
    }
}

I vissa fall måste en härledd klass åsidosätta basklassimplementeringen. Basklassmedlemmar som markerats med det abstrakta nyckelordet kräver att härledda klasser åsidosätter dem. När du försöker kompilera följande exempel genereras kompilatorfelet CS0534, "<klassen implementerar inte ärvd abstrakt medlemsmedlem><", eftersom klassen B inte tillhandahåller någon implementering för A.Method1> .

public abstract class A
{
    public abstract void Method1();
}

public class B : A // Generates CS0534.
{
    public void Method3()
    {
        // Do something.
    }
}

Arv gäller endast klasser och gränssnitt. Andra typkategorier (structs, ombud och uppräkningar) stöder inte arv. På grund av dessa regler genererar försök att kompilera kod som i följande exempel kompilatorfel CS0527: "Skriv "ValueType" i gränssnittslistan är inte ett gränssnitt." Felmeddelandet anger att arv inte stöds, även om du kan definiera de gränssnitt som en struct implementerar.

public struct ValueStructure : ValueType // Generates CS0527.
{
}

Implicit arv

Förutom alla typer som de kan ärva från via ett enda arv ärver alla typer i .NET-typsystemet implicit från Object eller en typ som härleds från det. De vanliga funktionerna Object i är tillgängliga för alla typer.

Om du vill se vad implicit arv innebär ska vi definiera en ny klass, SimpleClass, som helt enkelt är en tom klassdefinition:

public class SimpleClass
{ }

Du kan sedan använda reflektion (som gör att du kan granska en typs metadata för att få information om den typen) för att hämta en lista över de medlemmar som tillhör SimpleClass typen. Även om du inte har definierat några medlemmar i klassen SimpleClass indikerar utdata från exemplet att den faktiskt har nio medlemmar. En av dessa medlemmar är en parameterlös (eller standard) konstruktor som automatiskt tillhandahålls för SimpleClass typen av C#-kompilatorn. De återstående åtta är medlemmar i Object, den typ som alla klasser och gränssnitt i .NET-typsystemet slutligen implicit ärver.

using System.Reflection;

public class SimpleClassExample
{
    public static void Main()
    {
        Type t = typeof(SimpleClass);
        BindingFlags flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public |
                             BindingFlags.NonPublic | BindingFlags.FlattenHierarchy;
        MemberInfo[] members = t.GetMembers(flags);
        Console.WriteLine($"Type {t.Name} has {members.Length} members: ");
        foreach (MemberInfo member in members)
        {
            string access = "";
            string stat = "";
            var method = member as MethodBase;
            if (method != null)
            {
                if (method.IsPublic)
                    access = " Public";
                else if (method.IsPrivate)
                    access = " Private";
                else if (method.IsFamily)
                    access = " Protected";
                else if (method.IsAssembly)
                    access = " Internal";
                else if (method.IsFamilyOrAssembly)
                    access = " Protected Internal ";
                if (method.IsStatic)
                    stat = " Static";
            }
            string output = $"{member.Name} ({member.MemberType}): {access}{stat}, Declared by {member.DeclaringType}";
            Console.WriteLine(output);
        }
    }
}
// The example displays the following output:
//	Type SimpleClass has 9 members:
//	ToString (Method):  Public, Declared by System.Object
//	Equals (Method):  Public, Declared by System.Object
//	Equals (Method):  Public Static, Declared by System.Object
//	ReferenceEquals (Method):  Public Static, Declared by System.Object
//	GetHashCode (Method):  Public, Declared by System.Object
//	GetType (Method):  Public, Declared by System.Object
//	Finalize (Method):  Internal, Declared by System.Object
//	MemberwiseClone (Method):  Internal, Declared by System.Object
//	.ctor (Constructor):  Public, Declared by SimpleClass

Implicit arv från Object klassen gör dessa metoder tillgängliga för SimpleClass klassen:

  • Den offentliga ToString metoden, som konverterar ett SimpleClass objekt till dess strängrepresentation, returnerar det fullständigt kvalificerade typnamnet. I det här fallet ToString returnerar metoden strängen "SimpleClass".

  • Tre metoder som testar för likhet mellan två objekt: den offentliga instansmetoden Equals(Object) , den offentliga statiska Equals(Object, Object) metoden och den offentliga statiska ReferenceEquals(Object, Object) metoden. Som standard testar dessa metoder för referensjämlikhet. för att vara lika med måste två objektvariabler referera till samma objekt.

  • Den offentliga GetHashCode metoden, som beräknar ett värde som gör att en instans av typen kan användas i hashade samlingar.

  • Den offentliga GetType metoden, som returnerar ett Type objekt som representerar SimpleClass typen.

  • Den skyddade Finalize metoden, som är utformad för att frigöra ohanterade resurser innan ett objekts minne frigörs av skräpinsamlaren.

  • Den skyddade MemberwiseClone metoden, som skapar en ytlig klon av det aktuella objektet.

På grund av implicit arv kan du anropa alla ärvda medlemmar från ett SimpleClass objekt precis som om det faktiskt var en medlem som definierats i SimpleClass klassen. I följande exempel anropas SimpleClass.ToString till exempel metoden som SimpleClass ärver från Object.

public class EmptyClass
{ }

public class ClassNameExample
{
    public static void Main()
    {
        EmptyClass sc = new();
        Console.WriteLine(sc.ToString());
    }
}
// The example displays the following output:
//        EmptyClass

I följande tabell visas de kategorier av typer som du kan skapa i C# och de typer som de implicit ärver från. Varje bastyp gör en annan uppsättning medlemmar tillgängliga via arv till implicit härledda typer.

Typkategori Ärver implicit från
klass Object
Struct ValueType, Object
uppräkning Enum, , ValueTypeObject
Delegera MulticastDelegate, , DelegateObject

Arv och en "är en" relation

Vanligtvis används arv för att uttrycka en "is a"-relation mellan en basklass och en eller flera härledda klasser, där de härledda klasserna är specialiserade versioner av basklassen. den härledda klassen är en typ av basklass. Klassen representerar till exempel Publication en publikation av något slag, och Book klasserna och Magazine representerar specifika typer av publikationer.

Kommentar

En klass eller struct kan implementera ett eller flera gränssnitt. Även om gränssnittsimplementering ofta presenteras som en lösning för enskilt arv eller som ett sätt att använda arv med structs, är det avsett att uttrycka en annan relation (en "kan göra"-relation) mellan ett gränssnitt och dess implementeringstyp än arv. Ett gränssnitt definierar en delmängd av funktioner (till exempel möjligheten att testa för likhet, jämföra eller sortera objekt eller för att stödja kulturkänslig parsning och formatering) som gränssnittet gör tillgängligt för sina implementeringstyper.

Observera att "är en" också uttrycker relationen mellan en typ och en specifik instansiering av den typen. I följande exempel Automobile är en klass som har tre unika skrivskyddade egenskaper: Make, tillverkaren av bilen, Model, typen av bil och Year, dess tillverkningsår. Klassen Automobile har också en konstruktor vars argument tilldelas till egenskapsvärdena och åsidosätter Object.ToString metoden för att skapa en sträng som unikt identifierar instansen Automobile i stället Automobile för klassen.

public class Automobile
{
    public Automobile(string make, string model, int year)
    {
        if (make == null)
            throw new ArgumentNullException(nameof(make), "The make cannot be null.");
        else if (string.IsNullOrWhiteSpace(make))
            throw new ArgumentException("make cannot be an empty string or have space characters only.");
        Make = make;

        if (model == null)
            throw new ArgumentNullException(nameof(model), "The model cannot be null.");
        else if (string.IsNullOrWhiteSpace(model))
            throw new ArgumentException("model cannot be an empty string or have space characters only.");
        Model = model;

        if (year < 1857 || year > DateTime.Now.Year + 2)
            throw new ArgumentException("The year is out of range.");
        Year = year;
    }

    public string Make { get; }

    public string Model { get; }

    public int Year { get; }

    public override string ToString() => $"{Year} {Make} {Model}";
}

I det här fallet bör du inte förlita dig på arv för att representera specifika bilmodeller och bilmodeller. Du behöver till exempel inte definiera en Packard typ som representerar bilar som tillverkas av Packard Motor Car Company. I stället kan du representera dem genom att skapa ett Automobile objekt med lämpliga värden som skickas till dess klasskonstruktor, som i följande exempel.

using System;

public class Example
{
    public static void Main()
    {
        var packard = new Automobile("Packard", "Custom Eight", 1948);
        Console.WriteLine(packard);
    }
}
// The example displays the following output:
//        1948 Packard Custom Eight

En is-a-relation som baseras på arv tillämpas bäst på en basklass och på härledda klasser som lägger till ytterligare medlemmar i basklassen eller som kräver ytterligare funktioner som inte finns i basklassen.

Utforma basklassen och härledda klasser

Nu ska vi titta på processen med att utforma en basklass och dess härledda klasser. I det här avsnittet definierar du en basklass, Publication, som representerar en publikation av något slag, till exempel en bok, en tidning, en tidning, en tidskrift, en artikel osv. Du definierar också en Book klass som härleds från Publication. Du kan enkelt utöka exemplet för att definiera andra härledda klasser, till exempel Magazine, Journal, Newspaperoch Article.

Baspubliceringsklassen

När du utformar din Publication klass måste du fatta flera designbeslut:

  • Vilka medlemmar som ska ingå i basklassen Publication Publication och om medlemmarna tillhandahåller metodimplementeringar eller om Publication det är en abstrakt basklass som fungerar som mall för sina härledda klasser.

    I det här fallet Publication tillhandahåller klassen metodimplementeringar. Avsnittet Designa abstrakta basklasser och deras härledda klasser innehåller ett exempel som använder en abstrakt basklass för att definiera de metoder som härledda klasser måste åsidosätta. Härledda klasser kan tillhandahålla alla implementeringar som är lämpliga för den härledda typen.

    Möjligheten att återanvända kod (dvs. flera härledda klasser delar deklarationen och implementeringen av basklassmetoder och behöver inte åsidosätta dem) är en fördel med icke-abstrakta basklasser. Därför bör du lägga till Publication medlemmar i om deras kod sannolikt kommer att delas av vissa eller de flesta specialiserade Publication typer. Om du inte tillhandahåller grundläggande klassimplementeringar effektivt måste du tillhandahålla i stort sett identiska medlemsimplementeringar i härledda klasser i stället för en enda implementering i basklassen. Behovet av att underhålla duplicerad kod på flera platser är en potentiell källa till buggar.

    Både för att maximera återanvändning av kod och för att skapa en logisk och intuitiv arvshierarki vill du vara säker på att du i Publication klassen endast inkluderar de data och funktioner som är gemensamma för alla eller för de flesta publikationer. Härledda klasser implementerar sedan medlemmar som är unika för de specifika typer av publikationer som de representerar.

  • Hur långt du kan utöka din klasshierarki. Vill du utveckla en hierarki med tre eller flera klasser i stället för bara en basklass och en eller flera härledda klasser? Kan till exempel Publication vara en basklass för Periodical, som i sin tur är en basklass för Magazine, Journal och Newspaper.

    I ditt exempel använder du den lilla hierarkin för en Publication klass och en enda härledd klass, Book. Du kan enkelt utöka exemplet för att skapa ett antal ytterligare klasser som härleds från Publication, till exempel Magazine och Article.

  • Om det är meningsfullt att instansiera basklassen. Om den inte gör det bör du använda det abstrakta nyckelordet för klassen. Annars kan klassen Publication instansieras genom att anropa dess klasskonstruktor. Om ett försök görs att instansiera en klass som markerats med nyckelordet abstract av ett direktanrop till dess klasskonstruktor genererar C#-kompilatorn felet CS0144, "Det går inte att skapa en instans av den abstrakta klassen eller gränssnittet". Om ett försök görs att instansiera klassen med hjälp av reflektion genererar reflektionsmetoden en MemberAccessException.

    Som standard kan en basklass instansieras genom att anropa dess klasskonstruktor. Du behöver inte uttryckligen definiera en klasskonstruktor. Om det inte finns någon i basklassens källkod tillhandahåller C#-kompilatorn automatiskt en standardkonstruktor (parameterlös).

    I ditt exempel markerar Publication du klassen som abstrakt så att den inte kan instansieras. En abstract klass utan metoder abstract anger att den här klassen representerar ett abstrakt begrepp som delas mellan flera konkreta klasser (till exempel en Book, Journal).

  • Om härledda klasser måste ärva basklassimplementeringen av vissa medlemmar, om de har möjlighet att åsidosätta basklassimplementeringen eller om de måste tillhandahålla en implementering. Du använder det abstrakta nyckelordet för att tvinga härledda klasser att tillhandahålla en implementering. Du använder det virtuella nyckelordet för att tillåta att härledda klasser åsidosätter en basklassmetod. Som standard kan metoder som definierats i basklassen inte åsidosättas.

    Klassen Publication har inga abstract metoder, men själva klassen är abstract.

  • Om en härledd klass representerar den sista klassen i arvshierarkin och inte själv kan användas som basklass för ytterligare härledda klasser. Som standard kan alla klasser fungera som en basklass. Du kan använda det förseglade nyckelordet för att ange att en klass inte kan fungera som basklass för ytterligare klasser. Försök att härleda från ett förseglat klassgenererat kompilatorfel CS0509, "kan inte härledas från typnamn> av förseglad typ<".

    I ditt exempel markerar du din härledda klass som sealed.

I följande exempel visas källkoden Publication för klassen samt en PublicationType uppräkning som returneras av Publication.PublicationType egenskapen. Förutom de medlemmar som den ärver från ObjectPublication definierar klassen följande unika medlemmar och medlems åsidosättningar:


public enum PublicationType { Misc, Book, Magazine, Article };

public abstract class Publication
{
    private bool _published = false;
    private DateTime _datePublished;
    private int _totalPages;

    public Publication(string title, string publisher, PublicationType type)
    {
        if (string.IsNullOrWhiteSpace(publisher))
            throw new ArgumentException("The publisher is required.");
        Publisher = publisher;

        if (string.IsNullOrWhiteSpace(title))
            throw new ArgumentException("The title is required.");
        Title = title;

        Type = type;
    }

    public string Publisher { get; }

    public string Title { get; }

    public PublicationType Type { get; }

    public string? CopyrightName { get; private set; }

    public int CopyrightDate { get; private set; }

    public int Pages
    {
        get { return _totalPages; }
        set
        {
            if (value <= 0)
                throw new ArgumentOutOfRangeException(nameof(value), "The number of pages cannot be zero or negative.");
            _totalPages = value;
        }
    }

    public string GetPublicationDate()
    {
        if (!_published)
            return "NYP";
        else
            return _datePublished.ToString("d");
    }

    public void Publish(DateTime datePublished)
    {
        _published = true;
        _datePublished = datePublished;
    }

    public void Copyright(string copyrightName, int copyrightDate)
    {
        if (string.IsNullOrWhiteSpace(copyrightName))
            throw new ArgumentException("The name of the copyright holder is required.");
        CopyrightName = copyrightName;

        int currentYear = DateTime.Now.Year;
        if (copyrightDate < currentYear - 10 || copyrightDate > currentYear + 2)
            throw new ArgumentOutOfRangeException($"The copyright year must be between {currentYear - 10} and {currentYear + 1}");
        CopyrightDate = copyrightDate;
    }

    public override string ToString() => Title;
}
  • En konstruktor

    Publication Eftersom klassen är abstractkan den inte instansieras direkt från kod som i följande exempel:

    var publication = new Publication("Tiddlywinks for Experts", "Fun and Games",
                                      PublicationType.Book);
    

    Dess instanskonstruktor kan dock anropas direkt från härledda klasskonstruktorer, som källkoden Book för klassen visar.

  • Två publikationsrelaterade egenskaper

    Title är en skrivskyddad String egenskap vars värde anges genom att anropa Publication konstruktorn.

    Pages är en skrivskyddad Int32 egenskap som anger hur många totalt antal sidor publikationen har. Värdet lagras i ett privat fält med namnet totalPages. Det måste vara ett positivt tal eller ett ArgumentOutOfRangeException utlöses.

  • Utgivarrelaterade medlemmar

    Två skrivskyddade egenskaper och Publisher Type. Värdena tillhandahålls ursprungligen av anropet till Publication klasskonstruktorn.

  • Publiceringsrelaterade medlemmar

    Två metoder, Publish och GetPublicationDate, anger och returnerar publiceringsdatumet. Metoden Publish anger en privat published flagga till true när den anropas och tilldelar det datum som skickades till den som ett argument till det privata datePublished fältet. Metoden GetPublicationDate returnerar strängen "NYP" om published flaggan är falseoch värdet för datePublished fältet om det är true.

  • Upphovsrättsrelaterade medlemmar

    Metoden Copyright tar namnet på upphovsrättsinnehavaren och upphovsrättsinnehavarens år som argument och tilldelar dem till CopyrightName egenskaperna och CopyrightDate .

  • En åsidosättning av ToString metoden

    Om en typ inte åsidosätter Object.ToString metoden returnerar den det fullständigt kvalificerade namnet på typen, vilket är till liten nytta vid differentiering av en instans från en annan. Klassen Publication åsidosätter Object.ToString för att returnera värdet för Title egenskapen.

Följande bild illustrerar relationen mellan basklassen Publication och dess implicit ärvda Object klass.

Objekt- och publikationsklasserna

Klassen Book

Klassen Book representerar en bok som en specialiserad typ av publikation. I följande exempel visas källkoden Book för klassen.

using System;

public sealed class Book : Publication
{
    public Book(string title, string author, string publisher) :
           this(title, string.Empty, author, publisher)
    { }

    public Book(string title, string isbn, string author, string publisher) : base(title, publisher, PublicationType.Book)
    {
        // isbn argument must be a 10- or 13-character numeric string without "-" characters.
        // We could also determine whether the ISBN is valid by comparing its checksum digit
        // with a computed checksum.
        //
        if (!string.IsNullOrEmpty(isbn))
        {
            // Determine if ISBN length is correct.
            if (!(isbn.Length == 10 | isbn.Length == 13))
                throw new ArgumentException("The ISBN must be a 10- or 13-character numeric string.");
            if (!ulong.TryParse(isbn, out _))
                throw new ArgumentException("The ISBN can consist of numeric characters only.");
        }
        ISBN = isbn;

        Author = author;
    }

    public string ISBN { get; }

    public string Author { get; }

    public decimal Price { get; private set; }

    // A three-digit ISO currency symbol.
    public string? Currency { get; private set; }

    // Returns the old price, and sets a new price.
    public decimal SetPrice(decimal price, string currency)
    {
        if (price < 0)
            throw new ArgumentOutOfRangeException(nameof(price), "The price cannot be negative.");
        decimal oldValue = Price;
        Price = price;

        if (currency.Length != 3)
            throw new ArgumentException("The ISO currency symbol is a 3-character string.");
        Currency = currency;

        return oldValue;
    }

    public override bool Equals(object? obj)
    {
        if (obj is not Book book)
            return false;
        else
            return ISBN == book.ISBN;
    }

    public override int GetHashCode() => ISBN.GetHashCode();

    public override string ToString() => $"{(string.IsNullOrEmpty(Author) ? "" : Author + ", ")}{Title}";
}

Förutom de medlemmar som den ärver från PublicationBook definierar klassen följande unika medlemmar och medlems åsidosättningar:

  • Två konstruktorer

    De två Book konstruktorerna delar tre vanliga parametrar. Två, rubrik och utgivare, motsvarar konstruktorns Publication parametrar. Den tredje är författare, som lagras i en offentlig oföränderlig Author egenskap. En konstruktor innehåller en isbn-parameter som lagras i den ISBN automatiska egenskapen.

    Den första konstruktorn använder nyckelordet för att anropa den andra konstruktorn. Konstruktorlänkning är ett vanligt mönster för att definiera konstruktorer. Konstruktorer med färre parametrar ger standardvärden när konstruktorn anropas med det största antalet parametrar.

    Den andra konstruktorn använder basnyckelordet för att skicka rubrik- och utgivarnamnet till basklasskonstruktorn. Om du inte gör ett explicit anrop till en basklasskonstruktor i källkoden, levererar C#-kompilatorn automatiskt ett anrop till basklassens standardkonstruktor eller parameterlösa konstruktor.

  • En skrivskyddad ISBN egenskap som returnerar Book objektets internationella standardboksnummer, ett unikt 10- eller 13-siffrigt tal. ISBN levereras som ett argument till en av konstruktörerna Book . ISBN lagras i ett privat säkerhetskopieringsfält som genereras automatiskt av kompilatorn.

  • En skrivskyddad Author egenskap. Författarens namn anges som ett argument till båda Book konstruktorerna och lagras i egenskapen .

  • Två skrivskyddade prisrelaterade egenskaper Price och Currency. Deras värden anges som argument i ett SetPrice metodanrop. Egenskapen Currency är den tresiffriga ISO-valutasymbolen (till exempel USD för den amerikanska dollarn). ISO-valutasymboler kan hämtas från egenskapen ISOCurrencySymbol . Båda dessa egenskaper är externt skrivskyddade, men båda kan anges med kod i Book klassen.

  • En SetPrice metod som anger värdena för Price egenskaperna och Currency . Dessa värden returneras av samma egenskaper.

  • Åsidosättningar till ToString metoden (ärvd från Publication) och Object.Equals(Object) metoderna och GetHashCode (ärvda från Object).

    Om det inte åsidosätts Object.Equals(Object) , testar metoden för referensjämlikhet. Det innebär att två objektvariabler anses vara lika med om de refererar till samma objekt. Book I klassen ska å andra sidan två Book objekt vara lika med om de har samma ISBN.

    När du åsidosätter Object.Equals(Object) metoden måste du också åsidosätta GetHashCode metoden, som returnerar ett värde som körningen använder för att lagra objekt i hashade samlingar för effektiv hämtning. Hash-koden ska returnera ett värde som är konsekvent med testet för likhet. Eftersom du har åsidosatt Object.Equals(Object) för att returnera true om ISBN-egenskaperna för två Book objekt är lika returnerar du hash-koden som beräknas genom att anropa GetHashCode metoden för strängen som returneras av ISBN egenskapen.

Följande bild illustrerar relationen mellan Book klassen och Publication, dess basklass.

Publikations- och bokklasser

Nu kan du instansiera ett Book objekt, anropa både dess unika och ärvda medlemmar och skicka det som ett argument till en metod som förväntar sig en parameter av typen Publication eller av typen Book, som i följande exempel visas.

public class ClassExample
{
    public static void Main()
    {
        var book = new Book("The Tempest", "0971655819", "Shakespeare, William",
                            "Public Domain Press");
        ShowPublicationInfo(book);
        book.Publish(new DateTime(2016, 8, 18));
        ShowPublicationInfo(book);

        var book2 = new Book("The Tempest", "Classic Works Press", "Shakespeare, William");
        Console.Write($"{book.Title} and {book2.Title} are the same publication: " +
              $"{((Publication)book).Equals(book2)}");
    }

    public static void ShowPublicationInfo(Publication pub)
    {
        string pubDate = pub.GetPublicationDate();
        Console.WriteLine($"{pub.Title}, " +
                  $"{(pubDate == "NYP" ? "Not Yet Published" : "published on " + pubDate):d} by {pub.Publisher}");
    }
}
// The example displays the following output:
//        The Tempest, Not Yet Published by Public Domain Press
//        The Tempest, published on 8/18/2016 by Public Domain Press
//        The Tempest and The Tempest are the same publication: False

Utforma abstrakta basklasser och deras härledda klasser

I föregående exempel definierade du en basklass som tillhandahöll en implementering för ett antal metoder för att tillåta härledda klasser att dela kod. I många fall förväntas dock inte basklassen tillhandahålla någon implementering. I stället är basklassen en abstrakt klass som deklarerar abstrakta metoder. Den fungerar som en mall som definierar de medlemmar som varje härledd klass måste implementera. I en abstrakt basklass är implementeringen av varje härledd typ vanligtvis unik för den typen. Du markerade klassen med det abstrakta nyckelordet eftersom det inte var meningsfullt att instansiera ett Publication objekt, även om klassen gav implementeringar av funktioner som är gemensamma för publikationer.

Till exempel innehåller varje sluten tvådimensionell geometrisk form två egenskaper: området, formens inre omfattning; och perimeter, eller avståndet längs formens kanter. Hur dessa egenskaper beräknas beror dock helt på den specifika formen. Formeln för att beräkna en cirkels perimeter (eller omkrets) skiljer sig till exempel från en kvadrat. Klassen Shape är en abstract klass med abstract metoder. Det indikerar att härledda klasser har samma funktioner, men de härledda klasserna implementerar den funktionen på olika sätt.

I följande exempel definieras en abstrakt basklass med namnet Shape som definierar två egenskaper: Area och Perimeter. Förutom att markera klassen med det abstrakta nyckelordet markeras varje instansmedlem också med det abstrakta nyckelordet. I det här fallet Shape åsidosätter Object.ToString även metoden för att returnera namnet på typen i stället för dess fullständigt kvalificerade namn. Och det definierar två statiska medlemmar, GetArea och GetPerimeter, som gör det möjligt för anropare att enkelt hämta området och perimetern för en instans av en härledd klass. När du skickar en instans av en härledd klass till någon av dessa metoder anropar körningen metoden åsidosättning av den härledda klassen.

public abstract class Shape
{
    public abstract double Area { get; }

    public abstract double Perimeter { get; }

    public override string ToString() => GetType().Name;

    public static double GetArea(Shape shape) => shape.Area;

    public static double GetPerimeter(Shape shape) => shape.Perimeter;
}

Du kan sedan härleda vissa klasser från Shape som representerar specifika former. I följande exempel definieras tre klasser, Square, Rectangleoch Circle. Var och en använder en formel som är unik för just den formen för att beräkna området och perimetern. Vissa av de härledda klasserna definierar också egenskaper, till exempel och Circle.Diameter, som Rectangle.Diagonal är unika för den form som de representerar.

using System;

public class Square : Shape
{
    public Square(double length)
    {
        Side = length;
    }

    public double Side { get; }

    public override double Area => Math.Pow(Side, 2);

    public override double Perimeter => Side * 4;

    public double Diagonal => Math.Round(Math.Sqrt(2) * Side, 2);
}

public class Rectangle : Shape
{
    public Rectangle(double length, double width)
    {
        Length = length;
        Width = width;
    }

    public double Length { get; }

    public double Width { get; }

    public override double Area => Length * Width;

    public override double Perimeter => 2 * Length + 2 * Width;

    public bool IsSquare() => Length == Width;

    public double Diagonal => Math.Round(Math.Sqrt(Math.Pow(Length, 2) + Math.Pow(Width, 2)), 2);
}

public class Circle : Shape
{
    public Circle(double radius)
    {
        Radius = radius;
    }

    public override double Area => Math.Round(Math.PI * Math.Pow(Radius, 2), 2);

    public override double Perimeter => Math.Round(Math.PI * 2 * Radius, 2);

    // Define a circumference, since it's the more familiar term.
    public double Circumference => Perimeter;

    public double Radius { get; }

    public double Diameter => Radius * 2;
}

I följande exempel används objekt som härletts från Shape. Den instansierar en matris med objekt som härletts från Shape och anropar klassens statiska metoder Shape , som omsluter returegenskapsvärden Shape . Körningen hämtar värden från de åsidosatta egenskaperna för de härledda typerna. Exemplet omvandlar också varje Shape objekt i matrisen till dess härledda typ och hämtar, om gjutningen lyckas, egenskaperna för den specifika underklassen av Shape.

using System;

public class Example
{
    public static void Main()
    {
        Shape[] shapes = { new Rectangle(10, 12), new Square(5),
                    new Circle(3) };
        foreach (Shape shape in shapes)
        {
            Console.WriteLine($"{shape}: area, {Shape.GetArea(shape)}; " +
                              $"perimeter, {Shape.GetPerimeter(shape)}");
            if (shape is Rectangle rect)
            {
                Console.WriteLine($"   Is Square: {rect.IsSquare()}, Diagonal: {rect.Diagonal}");
                continue;
            }
            if (shape is Square sq)
            {
                Console.WriteLine($"   Diagonal: {sq.Diagonal}");
                continue;
            }
        }
    }
}
// The example displays the following output:
//         Rectangle: area, 120; perimeter, 44
//            Is Square: False, Diagonal: 15.62
//         Square: area, 25; perimeter, 20
//            Diagonal: 7.07
//         Circle: area, 28.27; perimeter, 18.85