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:
- 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.
- 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).