[C#] Covariant properties

Pagina: 1
Acties:

Acties:
  • 0 Henk 'm!

  • Victor
  • Registratie: November 2003
  • Niet online
Heb de afgelopen dagen gebruikt om mezelf wat in C# te verdiepen, maar ik loop nu tegen een "probleempje" aan waar ik zo snel geen elegante oplossing voor kan vinden. Ik probeer covariance toe te passen op properties, maar krijg dit nog niet voor elkaar. Ik heb voorbeelden gevonden van covariance d.m.v. delegates, maar dit biedt alleen een oplossing voor methods. Ik heb het nu met member hiding opgelost, maar ik vraag me af of er een elegantere oplossing is.

Omdat code meer zegt dan duizend woorden, een voorbeeld:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Car
{
    private Fuel fuel;
    
    public Fuel Fuel
    {
        get
        {
            return fuel;
        }
        set
        {
            fuel = value;
        }
    }
}

class Fuel
{
}

class SUV : Car
{
    new public Diesel Fuel
    {
        get
        {
            return base.Fuel as Diesel;
        }
        set
        {
            base.Fuel = value;
        }
    }
}

class Diesel : Fuel
{
}


Wat ik dus voor elkaar probeer te krijgen, is dat sub classes van een super class, ook alleen sub classes van hun properties accepteren. Zoals in dit geval een SUV die alleen Diesel als Fuel accepteert. Is er iemand die hier wellicht een betere oplossing voor heeft?

Acties:
  • 0 Henk 'm!

  • MTWZZ
  • Registratie: Mei 2000
  • Laatst online: 13-08-2021

MTWZZ

One life, live it!

Een mogelijke oplossing is dit:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public enum FuelType
{
    Gasoline,
    Diesel
}

public class Car
{
    private FuelType _fuel;

    public virtual FuelType Fuel
    {
        get { return _fuel; }
        set { _fuel = value; }
    }
}

public class SUV : Car
{
    public override FuelType Fuel
    {
        get { return base.Fuel; }
        set
        {
            if (value != FuelType.Diesel)
            {
                throw new Exception("A SUV runs on diesel");
            }
            base.Fuel = value;
        }
    }
}

Oftewel gewoon je argumenten checken. Eventueel zou je generics kunnen toepassen zodat je Car<Diesel> doet en het Fuel property dan alleen een Diesel accepteert maar dat vindt ik een beetje lelijk. Ook omdat je dan Diesel als type moet hebben (wat overigens best zou kunnen maar ik ken het scenario niet).

Nu met Land Rover Series 3 en Defender 90


Acties:
  • 0 Henk 'm!

  • Woy
  • Registratie: April 2000
  • Niet online

Woy

Moderator Devschuur®
Ik zou het in dit geval inderdaad met Generics doen
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
abstract class Fuel {}
class Diesel : Fuel {}
class Petrol : Fuel {}

class Car<T> where T : Fuel
{
    public T Fuel { get; set; }
}

class DieselCar : Car<Diesel>{}

class PetrolCar : Car<Petrol> {}

class SUV : DieselCar{}

of eventueel virtual properties maken en overidden ( zoals MTWZZ ook al aangeeft )

C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Car
{
    virtual Fuel Fuel { get; set; }
}

public class DieselCar : Car
{
    override Fuel Fuel
    {
         get { ... }
         set
         {
             if( value is Diesel ){ base.Fuel = value; }
             else { throw new InvalidFuelTypeException(); }
         }
    }
}

[ Voor 3% gewijzigd door Woy op 29-12-2008 11:35 ]

“Build a man a fire, and he'll be warm for a day. Set a man on fire, and he'll be warm for the rest of his life.”


Acties:
  • 0 Henk 'm!

  • prototype
  • Registratie: Juni 2001
  • Niet online

prototype

Cheer Bear

rwb schreef op maandag 29 december 2008 @ 11:32:
Ik zou het in dit geval inderdaad met Generics doen
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
abstract class Fuel {}
class Diesel : Fuel {}
class Petrol : Fuel {}

class Car<T> where T : Fuel
{
    public T Fuel { get; set; }
}

class DieselCar : Car<Diesel>{}

class PetrolCar : Car<Petrol> {}

class SUV : DieselCar{}
Let wel op dat de covariance die je hierbij toepast het ook mogelijk maakt om Petrol te geven aan een DieselCar, i.e.

C#:
1
class DieselCar : Car<Petrol> {}


Is ook valid hierbij. Dat is fundamenteel anders dan je andere oplossing :-).

[ Voor 10% gewijzigd door prototype op 29-12-2008 12:02 ]


Acties:
  • 0 Henk 'm!

  • Woy
  • Registratie: April 2000
  • Niet online

Woy

Moderator Devschuur®
prototype schreef op maandag 29 december 2008 @ 11:58:
[...]

Let wel op dat de covariance die je hierbij toepast het ook mogelijk maakt om Petrol te geven aan een DieselCar, i.e.

C#:
1
class DieselCar : Car<Petrol> {}
Ja maar dan wijzig je de definitie van DieselCar, en dan is dus je naamgeving gewoon fout, want als je hem zo defineert kun je aan een DieselCar geen Diesel geven, maar alleen Petrol.

Als je DieselCar van Car<Diesel> laat inherriten kan je nooit meer Petrol als Fuel meegeven.
Dat is fundamenteel anders dan je andere oplossing :-).
Het is idd beide fundamenteel anders, maar in beide gevallen kun je alleen Diesel bij een DieselCar assignen, alleen is het bij het eerste voorbeeld at compile time, en bij het 2e at run time.

[ Voor 7% gewijzigd door Woy op 29-12-2008 12:31 ]

“Build a man a fire, and he'll be warm for a day. Set a man on fire, and he'll be warm for the rest of his life.”


Acties:
  • 0 Henk 'm!

  • EfBe
  • Registratie: Januari 2000
  • Niet online
Het probleem heeft overigens niets met covariance te maken, maar met property value validation.

Creator of: LLBLGen Pro | Camera mods for games
Photography portfolio: https://fransbouma.com


Acties:
  • 0 Henk 'm!

  • prototype
  • Registratie: Juni 2001
  • Niet online

prototype

Cheer Bear

rwb schreef op maandag 29 december 2008 @ 12:30:
[...]

Ja maar dan wijzig je de definitie van DieselCar, en dan is dus je naamgeving gewoon fout, want als je hem zo defineert kun je aan een DieselCar geen Diesel geven, maar alleen Petrol.

Als je DieselCar van Car<Diesel> laat inherriten kan je nooit meer Petrol als Fuel meegeven.
Klopt, het is alleen even iets dat ik nog aan wilde geven terwijl ik op m'n koffie wacht ;-) Nu ik daar nog steeds op aan het wachten ben, vraag ik me af of deze type nu invariant is zoals dat bij Java generics zo het geval is, i.e. kan ik een DieselCar nu alleen echt maar --exact-- Diesel geven of ook een subtype van Diesel? Natuurlijk zou ik eventueel gewoon kunnen casten dan bij assignen naar Diesel, maar ik vraag me dus af of C# generics zoiets heeft als wildcards. In dat geval zou ik eerder zoiets schrijven en gebruik daarbij even Java syntax omdat de generics daarvan iets meer recent nog in 't geheugen liggen:
Java:
1
2
3
4
5
6
7
8
9
10
class Fuel { }
class Diesel extends Fuel { }
class SuperDiesel extends Diesel {}

class Car<T extends Fuel> {
    public <? extends T> fuel;
}

class DieselCar extends Car<Diesel> {
}

Nu zou ik dan theoretisch gezien ook dit kunnen doen:
Java:
1
2
Car<Diesel> c = new DieselCar();
c.fuel = new SuperDiesel();

In plaats van:
Java:
1
2
Car<Diesel> c = new DieselCar();
c.fuel = (Diesel)new SuperDiesel();

Acties:
  • 0 Henk 'm!

  • Victor
  • Registratie: November 2003
  • Niet online
Bedankt voor alle goede tips! Ik was helemaal niet bekend met generics, dus ben me nu even aan't inlezen op de MSDN library. Dit lijkt inderdaad wel de richting te zijn waarin ik het zoeken moet (d.w.z., het werkt ook in mijn daadwerkelijke code en niet alleen in mijn abstracte voorbeeld).

Acties:
  • 0 Henk 'm!

  • Victor
  • Registratie: November 2003
  • Niet online
rwb schreef op maandag 29 december 2008 @ 11:32:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
abstract class Fuel {}
class Diesel : Fuel {}
class Petrol : Fuel {}

class Car<T> where T : Fuel
{
    public T Fuel { get; set; }
}

class DieselCar : Car<Diesel>{}

class PetrolCar : Car<Petrol> {}

class SUV : DieselCar{}
Hoe zou ik in dit geval een constraint kunnen leggen op het (generic) type Car? Bijvoorbeeld:
C#:
1
2
3
4
5
6
abstract class CarPark<T>
{
    public void parkCar(T car)
    {
    }
}

Ik wil hier nu dus een constraint leggen op type T, zodat deze enkel en alleen objecten van het generic type Car accepteert. Dit lijkt helaas niet te werken:
C#:
1
abstract class CarPark<T> where T : Car

De compiler wil toch wel graag dat ik het type argument meegeef aan het Car type ("Using the generic type 'Car<T>' requires '1' type arguments"). Nu kan ik het generic Car type wel weer van een andere class laten erven en daar de constraint op leggen, maar zoals altijd zoek ik eigenlijk naar een elegantere oplossing. Kunnen jullie me wellicht hier ook nog een zetje mee in de goede richting geven?

Acties:
  • 0 Henk 'm!

  • .oisyn
  • Registratie: September 2000
  • Laatst online: 22-09 16:37

.oisyn

Moderator Devschuur®

Demotivational Speaker

C#:
1
abstract class CarPark<T, U> where T : Car<U>

Nadeel is dan wel dat je vervolgens zowel het type auto als het type brandstof mee moet geven. Maar waar je sowieso aan moet denken is dat je met generics geen basetype definieert. Er is niet zoiets als een Car. Er zijn alleen Cars met een concreet brandstof, dus Car<Gasoline> en Car<Diesel>. Overal in de code waar je niet geïnteresseerd bent in de brandstof zul je dus toch generic moeten maken. Het is dus sowieso handig om een basetype Car te maken die niet generiek is, en Car<T> daarvan af te laten leiden. CarPark kan dan gewoon dat basetype als constraint gebruiken.

Give a man a game and he'll have fun for a day. Teach a man to make games and he'll never have fun again.


Acties:
  • 0 Henk 'm!

  • Victor
  • Registratie: November 2003
  • Niet online
.oisyn schreef op dinsdag 30 december 2008 @ 22:51:
C#:
1
abstract class CarPark<T, U> where T : Car<U>

Nadeel is dan wel dat je vervolgens zowel het type auto als het type brandstof mee moet geven. Maar waar je sowieso aan moet denken is dat je met generics geen basetype definieert. Er is niet zoiets als een Car. Er zijn alleen Cars met een concreet brandstof, dus Car<Gasoline> en Car<Diesel>. Overal in de code waar je niet geïnteresseerd bent in de brandstof zul je dus toch generic moeten maken. Het is dus sowieso handig om een basetype Car te maken die niet generiek is, en Car<T> daarvan af te laten leiden. CarPark kan dan gewoon dat basetype als constraint gebruiken.
Begon zelf ook langzamerhand tot die conclusie te komen na te hebben gelezen hoe de CLR met generic types omspringt. Zal dus inderdaad met een base type aan de slag moeten.

De oplossing die je aandracht was bij mijn trouwens ook al de revue gepasseerd, maar dat vereist mijns inziens te veel wetenschap van de totstandkoming van het type (dus dat DieselCar van het generic type Car afkomt, met als type T Diesel), wat niet handig is. Nu is dat in dit voorbeeld nog wel duidelijk, maar zoals je al zegt, je wil je daar niet overal in je code mee bezig hoeven houden.

In ieder geval bedankt voor je toelichting, ik ga weer verder knutselen!

Acties:
  • 0 Henk 'm!

  • Woy
  • Registratie: April 2000
  • Niet online

Woy

Moderator Devschuur®
Je zou eventueel ook nog een non-generic car kunnen maken, als het niet uithaalt wat voor type fuel een car heeft
C#:
1
2
3
4
5
6
7
8
9
10
11
12
abstract class Car
{
    abstract void Drive();
}

abstract class Car<T> where T : fuel
{
    virtual void Drive() { ... }
    
    T Fuel { get; set; }
}
class CarPark<T> where T : Car


edit:
o niet goed gelezen, dat is precies wat er voorgesteld werd :)

[ Voor 9% gewijzigd door Woy op 31-12-2008 09:01 ]

“Build a man a fire, and he'll be warm for a day. Set a man on fire, and he'll be warm for the rest of his life.”

Pagina: 1