webForumDet fria alternativet

Räkna instanser av en klass [C#]

.NET

19 svar · 2 506 visningar · startad av Gildebrand

Medlem sedan juni 2009920 inlägg
Frågan#1

Hejsan!

Försöker lära mig OOP (har inte förstått vad det handlat om tidigare), och har börjat leka lite med det nu i VS. Snabb fråga, finns det något enkelt sätt att räkna antalet instanser av en klass?

Medlem sedan mars 20007 896 inlägg
#2
class CountMe {
    private static int instanceCounter = 0;

    public CountMe() {
        instanceCounter++;
    }

    public static int getInstanceCount() {
        return instanceCounter;
    }
}

...
Console.Write("Instances: " + CountMe.getInstanceCount());

Statiska variabler och metoder är instansoberoende, och svarar mot en klass (därför kallas de ibland för klassvariabler).

Medlem sedan juni 2009920 inlägg
#3

Funkade finfint det :)
För att jag ska förstå hur det funkar.

Där körs alltså Public CountMe() så fort man skapar en instans av klassen CountMe?

Och

public static int InstanceCount()
        {
            return instanceCounter;
        }

Är en egenskap av klassen, och inte en egenskap för en instans av klassen? Det åstadkommer man genom static va?

Medlem sedan mars 20007 896 inlägg
#4

Ja, "public CountMe()" definierar konstruktorn för klassen, vilken alltid körs så fort det skapas en instans. Variabler och metoder deklarerade som "static" är, som jag sa, instansoberoende - dom existerar i klassen och inte i en instans.

Medlem sedan juli 2003555 inlägg
#5

En varning är dock på sin plats: Den där konstruktionen är inte trådsäker. Skulle du köra multitrådat så finns risken att två trådar försöker komma åt samma variabel samtidigt, vilket inte är trevligt

Medlem sedan juni 2009920 inlägg
#6

Hur ska man göra det hela trådsäkert då?
Jag är lite fundersam på det här med instanserna av en klass. Jag har en klass som heter Car, sen skapar jag (konstruerar?) en instans av den klassen som heter bil1. Sen kan jag skapa en till instans av klassen, med samma namn. Det borde ju inte ens gå?

Medlem sedan mars 20007 896 inlägg
#7

Det lättaste sättet att hantera flera trådar mot samma objekt, är att låsa det delade objektet med nyckelordet 'lock'. http://en.csharp-online.net/Building_Multithreaded_Applications—The_Issue_of_Concurrency

Arbetar du med kollektioner som är delade över flera trådar finns det färdiga Concurrency-kollektioner att tillgå, det kommer också att tillkomma tre till i och med .NET 4.0.

Medlem sedan aug. 20003 575 inlägg
#8

Jag tycker lösningen blir ful.
Blir snyggare om du har en Bilfabrik som håller koll på instanserna.

Medlem sedan juni 2009920 inlägg
#9

Jo jag funderar på det, då kan jag ju även hålla reda på alla instanserna där.

Medlem sedan juni 2009920 inlägg
#10

Det finns inget annat smart sätt att hålla reda på alla instanser av en klass än att typ spara undan instansens namn i en list?

Medlem sedan juli 2003555 inlägg
#11

Till att börja med, det finns inget namn på en instans. Däremot så har du ett namn på din variabel (i alla fall ända tills kompilatorn lagt beslag på din kod, då försvinner namnet på variabeln också.. ;) )

En variabel kan vara en pekare till någonting, t.ex. ett objekt (dvs. en instans av en valfri klass.) Sedan kan allt ifrån 0 till oändligt antal variabler peka på samma objekt. I C++, när man använder "smarta pekare", så ser C++ till att döda, dvs. köra destruktorn på objekt som inte längre har några pekare på sig, i .NET-världen däremot, där körs inte destruktorn förrens .NET tycker att det är dåligt med minne, eller någonting annat godtyckligt (det finns nog inte ens någon garanti för att den någonsin kommer köras.)

Vad leder det här då till? Jo, bara för att du dödar den sista pekaren till variabeln (t.ex. för att variabeln går out-of-scope pga. att funktionen tog slut, eller att du satte variabeln till null etc. etc.) så betyder inte det att objektet försvinner. Det kan leva kvar länge, länge..

När man jobbar med vissa typer av resurser, t.ex. filer, nätverk, annat IO-grunk som inte finns inbyggt i .NET ifrån början (dvs. i bakgrunden anropar andra API:er) så kan man se det här som ett problem: när objektet överges (inga pekare längre) så är det dumt att objektet lägger beslag på en massa resurser till ingen nytta. Potentiell minnesdödare. Att lägga kod för att släppa de här resurserna i destruktorn hjälper ingenting, den kanske aldrig körs.. Lösningen på det problemet heter IDisposable.

Om din klass implementerar interfacet IDisposable så tvingas du skapa metoden void Dispose(), vars enda uppgift är att se till att döda externa resurser osv. Du måste dock själv ansvara för att anropa den, men interfacet IDisposable är "standard" i .NET så man "vet" att man ska anropa den om den finns. Titta t.ex. på using(){} i C#.

Om vi nu ska återgå till problemet så, nej, det finns inget "smart" sätt. Det är du som måste hålla kollen, hur du gör är upp till dig, men tänk på trådproblematiken. Förr eller senare sitter du troligtvis där att du har multitrådat, hur du hanterar det är upp till dig (t.ex. skapa och förstör bara objekt på en viss tråd, eller se till att låsa dina räknare/listor innan du uppdaterar dem.) Förslagsvis kan du lägga kod för att räkna ner i dina Dispose().

Medlem sedan juni 20008 205 inlägg
#12

SPiN skrev:

Det lättaste sättet att hantera flera trådar mot samma objekt, är att låsa det delade objektet med nyckelordet 'lock'. http://en.csharp-online.net/Building_Multithreaded_Applications—The_Issue_of_Concurrency

... men Interlocked.Increment torde vara effektivare i det här fallet (och kräver inte att man håller reda på låsobjekt etc).

class CountMe {
    private static long instanceCounter = 0;

    public CountMe() {
        Interlocked.Increment(ref instanceCounter);
    }

    public static int InstanceCount {
        get { return Interlocked.Read(ref instanceCounter); }
    }
}

Sen har jag som så många andra i den här tråden lite svårt att se syftet med att räkna instanser annat än för diagnostik (och då har man antagligen roligare med en heapdump istället). Ska man nödvändigtvis ha det för något annat syfte håller jag med om att det är en bättre idé att ha nån slags factory som håller reda på hur många bilar den skapat, så att olika delar av programmet kan skapa bilar utan att förstöra för varandra (plus att mängden magi i systemet då minskas).

Medlem sedan mars 20007 896 inlägg
#13

Väldigt snygg lösning med Interlocked, spango! Nu är jag ingen .NET-programmerare, så jag har bara givit de lättaste svaren för generell OOP - men självklart finns det bättre och effektivare sätt att lösa "problemet" på än att ha en statisk räknare. Det som efterfrågades var ett enkelt sätt att räkna instanser och sedan ett enkelt sätt att hantera multipla trådar - och då är det fortfarande lättast att ha en statisk räknare och att använda ett lås (lock eller Interlocked då).

Medlem sedan juni 2009920 inlägg
#14

Ska vara ärlig, förstod inte särskilt mycket av det där ni skrev, men jag ska lära mig, och försöka förstå det iallafall :)

Medlem sedan juni 2009920 inlägg
#15

Jag hade en liten idé om hur man skulle kunna hålla reda på alla bilarna, man placerar dem i en list. Jag tänkte något sånt här:

            List<Car> car = new List<Car>();
            car.Add(new Car{ //Attributen här })

Klasserna för hjulen och bilen ser ut såhär:

    class Car
    {
        static long instanceCounter = 0;
        public int MaxWheelSize = 20;
        public int MinWheelSize = 12;
        public Wheel FrontLeft;
        public Wheel FrontRight;
        public Wheel RearLeft;
        public Wheel RearRight;

        public Car()
        {
            Interlocked.Increment(ref instanceCounter);
            FrontLeft=new Wheel(this);
            this.FrontRight = new Wheel(this);
            this.FrontLeft = new Wheel(this);
            this.RearLeft = new Wheel(this);
            this.RearRight = new Wheel(this);
        }
        public static long InstanceCount
        {
            get {return Interlocked.Read(ref instanceCounter);}
        }

    }
    class Wheel
    {
        private int size;
        private Car parentCar;
        public Wheel(Car thisCar)
        {
            this.parentCar = thisCar;
        }

        public int Size
        {
            get
            {
                return this.size;
            }
            set
            {
                    if (value >= this.parentCar.MinWheelSize && value <= this.parentCar.MaxWheelSize)
                    {
                        this.size = value;
                    }
                    else
                    {
                        if (value < this.parentCar.MinWheelSize)
                        {
                            Console.WriteLine("För litet hjul!");
                        }
                        if (value > this.parentCar.MaxWheelSize)
                        {
                            Console.WriteLine("För stort hjul!");
                        }
                    }
                }
            }
    }

Jag trodde att jag skulle kunna skriva såhär:

            car.Add(new Car{FrontLeft.Size=14})

Men det går inte. Intellisense hittar på FrontLeft, men efter punkten så dyker det inte upp något mer, FrontLeft.Size verkar inte funka där, men vad ska jag göra för att få det att funka?

Medlem sedan juni 20008 205 inlägg
#16

Det är bara klassens egna properties/fält som går att sätta på det viset i en initieringsuttryck. Om du inte vill sätta hjulstorlekarna explicit får du göra en konstruktor eller en statisk factorymetod som gör det åt dig:

    class Car
    {

        public int MaxWheelSize = 20;
        public int MinWheelSize = 12;
        public Wheel FrontLeft;
        public Wheel FrontRight;
        public Wheel RearLeft;
        public Wheel RearRight;

        public Car()
        {
            this.FrontRight = new Wheel(this);
            this.FrontLeft = new Wheel(this);
            this.RearLeft = new Wheel(this);
            this.RearRight = new Wheel(this);
        }

        // Konstruktor:
        public Car(int frontLeft, int frontRight, int rearLeft, int rearRight) : this()
        {
            this.FrontLeft.Size = frontLeft;
            this.FrontRight.Size = frontRight;
            this.FrontLeft.Size = frontLeft;
            this.RearLeft.Size = rearLeft;
        }

        // Statisk factorymetod:
        public Car WithWheelSizes(int frontLeft, int frontRight, int rearLeft, int rearRight) 
        {
            var c = new Car();
            c.FrontLeft.Size = frontLeft;
            c.FrontRight.Size = frontRight;
            c.FrontLeft.Size = frontLeft;
            c.RearLeft.Size = rearLeft;
            return c;
        }

    }

Jag brukar föredra factorymetoder, eftersom det blir lättare att förstå vad Car.WithWheelSizes(12, 12, 12, 12) betyder än new Car(12, 12, 12, 12). Konstruktorer har ju dock fördelen att de kan användas av subklasser (så i de fall där man vill tillåta subklassning - vilket är förvånansvärt sällan - är konstruktorn det bättre alternativet).

Sen finns det lite andra grejer som man kanske ska komma med konstruktiv kritik på, om det är OK?

Det första är att man egentligen inte bör ha publika fält på det sättet som du har, utan properties. Vill man lägga till t.ex. validering måste man ha en property, och även om fält och properties må se likadana ut i koden, är de inte binärt kompatibla (dvs de blir helt olika saker när programmet kompilerats), och det finns vissa andra skillnader också (t.ex. kan du använda ett fält som out/ref-parameter, men inte en property). I C# blir den kodmässiga skillnaden ganska minimal när man ändrar till properties istället:

    class Car
    {
        public int MaxWheelSize = 20;
        public int MinWheelSize = 12;
        public Wheel FrontLeft { get; set; }
        public Wheel FrontRight { get; set; }
        public Wheel RearLeft { get; set; }
        public Wheel RearRight { get; set; }

        // Konstruktorer etc enligt ovan

    }

Sen är det inte helt bra OO med reflexiva beroenden som du har, med Car och Wheel som refererar till varandra. Det finns två skäl till varför du vill undvika det:

  1. Man ska aldrig skicka this från en konstruktor till en metod utanför klassen. Anledningen är att objektet inte är helt färdigbyggt, vilket kan skapa oändliga mängder strul om du lägger kod i Wheel-klassens konstruktor som utgår från att den Car som konstruktorn får är färdigbyggd.
  2. Dubbelriktade beroenden orsakar hårdare koppling. Att man inte kan ha en bil utan hjul kan jag acceptera (även om man i framtiden inte kommer behöva det när man har flygande bilar ;)), men det känns inte som att hjulen ska behöva känna till bilen. Tänk vad sura Hell's Angels blir när de inte får hjul till sina bågar...

Så för att få lite snyggare OO bryter vi ut det som Wheel vill ha från Car till en egen klass:

    class SizeConstraint
    {
        public int Max { get; set; }
        public int Min { get; set; }
    }

    class Car
    {

        public SizeConstraint WheelSize { get; set; }
        public Wheel FrontLeft { get; set; }
        public Wheel FrontRight { get; set; }
        public Wheel RearLeft { get; set; }
        public Wheel RearRight { get; set; }

        public Car()
        {
            this.WheelSize = new SizeConstraint{ Min = 12, Max = 20 };
            this.FrontRight = new Wheel(this.WheelSize);
            this.FrontLeft = new Wheel(this.WheelSize);
            this.RearLeft = new Wheel(this.WheelSize);
            this.RearRight = new Wheel(this.WheelSize);
        }

        // etc

    }

    class Wheel
    {
        private int size;
        private readonly SizeConstraint sizeConstraint;
        public Wheel(SizeConstraint sizeConstraint)
        {
            this.sizeConstraint = sizeConstraint;
        }

        public int Size
        {
            get
            {
                return this.size;
            }
            set
            {
                    if (value >= this.sizeConstraint.Min && value <= sizeConstraint.Max)
                    {
                        this.size = value;
                    }
                    else
                    {
                        if (value < this.sizeConstraint.Min)
                        {
                            Console.WriteLine("För litet hjul!");
                        }
                        if (value > this.sizeConstraint.Max)
                        {
                            Console.WriteLine("För stort hjul!");
                        }
                    }
                }
            }
    }

Så, nu kan vi ha hjul som existerar utan sitta på bilar. Motorcyklisterna blir skitnöjda och bjuder dig på öl!

Nästa steg är att du borde kasta ett exception från Wheel.Size om nån försöker sätta ett värde som inte är inom det giltiga spannet, istället för att skriva ett felmeddelande i konsollen. I ditt fall borde ArgumentOutOfRangeException sitta som en smäck, och vi låter SizeConstraint göra valideringen:

    class SizeConstraint
    {
        public int Max { get; set; }
        public int Min { get; set; }

        public void Validate(int value)
        {
            if (value < Min) throw new ArgumentOutOfRangeException(value + " < " + Min);
            if (value > Max) throw new ArgumentOutOfRangeException(value + " > " + Max);
        }
    }

    class Wheel
    {
        private int size;
        private readonly SizeConstraint sizeConstraint;
        public Wheel(SizeConstraint sizeConstraint)
        {
            this.sizeConstraint = sizeConstraint;
        }

        public int Size
        {
            get
            {
                return this.size;
            }
            set
            {
                this.sizeConstraint.Validate(value); // om value är felaktigt kastar Validate ett exception
                this.size = value;
            }
        }
    }

Mindre och korrektare kod - sweet! Man dock fortfarande orsaka en del problem:

// Variant 1:
var c = Car.WithWheelSizes(15, 15, 15, 10); // Aningens sned bil...
c.WheelSize.Max = 14;                       // lol wut?
c.RearRight.Size = c.RearLeft.Size;         // Försöker göra bilen stabil - får exception :(

// Variant 2:
var c = Car.WithWheelSizes(15, 15, 15, 15); // Stabilt fordon
c.WheelSize.Max = 15;                       // Däcken får inte bli större än de är
var hax = new Wheel(                        // Skapa ett hjul utan bil
    new SizeConstraint()
    {
        Min = 10, Max = 100
    }
){ Size = 20 }; 
c.RearRight = c.RearLeft = c.FrontLeft = c.FrontRight = hax; // WTF!? Hjulen större än tillåten maxstorlek

Det enklaste sättet att undvika sånt här är att göra objekt "immutable" - så att de inte går att ändra på efter att man skapat dem (vill man "modifiera" dem får man göra en kopia med några värden ändrade). Det känns intuitivt väldigt oflexibelt och ineffektivt att göra på det viset, men så är det inte nödvändigtvis. Det blir ofantligt mycket lättare att skriva korrekt kod om man vet att ens objekt inte ändras, och ska man tänka på prestanda är allokering av nya objekt väldigt, väldigt billigt, och garbage collectors är utformade så att det är mer eller mindre gratis att samla upp objekt som bara levt en kort tid.

    sealed class SizeConstraint 
    {
        public int Max { get; private set; }
        public int Min { get; private set; }

        public SizeConstraint(int min, int max)
        {
            this.Min = min;
            this.Max = max;
            Validate(this.Min);
            Validate(this.Max);
        }
        public void Validate(int value)
        {
            if (value < Min) throw new ArgumentOutOfRangeException(value + " < " + Min);
            if (value > Max) throw new ArgumentOutOfRangeException(value + " > " + Max);
        }
    }

    class Car
    {

        public Wheel FrontLeft { get; private set; } 
        public Wheel FrontRight { get; private set; }
        public Wheel RearLeft { get; private set; }
        public Wheel RearRight { get; private set; }

        private static readonly SizeConstraint DefaultWheelConstraint = new SizeConstraint{ Min = 12, Max = 20 };

        public Car() : this(DefaultWheelConstraint)
        {
        }

        public Car(SizeConstraint wheelSizeRange)
        {
            this.FrontRight = new Wheel(wheelSizeRange);
            this.FrontLeft = new Wheel(wheelSizeRange);
            this.RearLeft = new Wheel(wheelSizeRange);
            this.RearRight = new Wheel(wheelSizeRange);
        }

        public Car WithWheelSizes(int frontLeft, int frontRight, int rearLeft, int rearRight) 
        {
            return WithWheelSizes(DefaultWheelConstraints, frontLeft, frontRight, rearLeft, rearRight);
        }

        public Car WithWheelSizes(SizeConstraint wheelSizeRange, int frontLeft, int frontRight, int rearLeft, int rearRight) 
        {
            var c = new Car(wheelSizeRange);
            c.FrontLeft.Size = frontLeft;
            c.FrontRight.Size = frontRight;
            c.FrontLeft.Size = frontLeft;
            c.RearLeft.Size = rearLeft;
            return c;
        }

    }

    class Wheel
    {
        private int size;
        private readonly SizeConstraint sizeConstraint;
        public Wheel(SizeConstraint sizeConstraint)
        {
            this.sizeConstraint = sizeConstraint;
            this.Size = this.sizeConstraint.Min; // Någorlunda vettigt defaultvärde för hjulstorlek
        }

        public int Size
        {
            get
            {
                return this.size;
            }
            set
            {
                this.sizeConstraint.Validate(value); // om value är för stort/litet kastar Validate ett exception
                this.size = value;
            }
        }
    }

Några små förändringar gör oss immuna mot mina felanvändningar ovan:

  • SizeConstraint är nu immutable, vilket innebär att vi inte längre behöver oroa oss för att någon ändrar tillåten min- eller maxstorlek så att hjulens nuvarande storlek blir ogiltig. (Klasser för objekt som ska vara immutable brukar deklareras som sealed, så att ingen ärver och override:ar något beteende.)
  • Car måste ta en SizeConstraint för hjulen till konstruktorn, eftersom vi inte kan ändra på SizeConstrainten i efterhand.
  • Ingen kan byta ut Wheel-objekt på en Car utan att gå genom Car-objektet, eftersom alla Wheel-setters är privata. (Alla kan fortfarande dock pilla på hjulen vilket egentligen inte är bra, men den här posten är redan för TL;DR för att ta upp det, plus att det skrivits massor om Law of Demeter här på forumet, så sök och du skall finna).
Medlem sedan juni 2009920 inlägg
#17

Där lärde jag mig mycket :)
Tog lite tid dock innan jag hann prova. Kände inte till det där med factorymetoder innan.
Tack för ditt inlägg!

Medlem sedan juni 2009920 inlägg
#18

Har en till fråga, som borde vara ganska enkel, och handlar bara om "effektivare" kodning.

Kan man deklarera flera objekt på en gång, utan att behöva upprepa samma sak flera gånger? Tidigare deklarerade jag ju fyra hjul, med denna kod:

        public wheel FL;
        public wheel FR;
        public wheel RL;
        public wheel RR;

Kan man inte deklarera FL,FR,RL,RR på samma gång istället för att de ska ha en varsin rad?
Typ något sånt här

public wheel FL & FR & RL & RR;
Medlem sedan juni 2009920 inlägg
#19

Nu löste det sig med det jag skrev i inlägget ovan, för när man deklarerar objekten, men hur gör man när man ska skapa objekten?

Det funkar inte att skriva

FL, FR, RL, RR = new wheel(this);
Medlem sedan aug. 20003 575 inlägg
#20

På vilket sätt blir det effektivare kodning?
Jag tycker det ser ut som att du gör det krångligare och mer svårtläst.

Ett tips är att inte förkorta heller utan skriv hela namnen FrontLeft, FrontRight osv..
Då är det enklare och går snabbare att läsa din kod.

270 ms totalt · 4 externa anrop · v20260731065814-full.86ec41c2
125 ms — deklarationer (db)
0 ms — hämta statistik (cache)
142 ms — hämta tråd, inlägg och bilagor (db)
117 ms — ändringar (db)