简体   繁体   中英

Create a constructor property that can be more than one type?

Hi I am curious to know how to create a constructor property in C# that can be more than one type.

I have looked at stack overflow links:

How to parse JSON string that can be one of two different strongly typed objects?

A variable of two types

...but they don't seem to match my inquiry.

I can produce what I want in python as per below. IE initialise an object of class "this_thing" that can take an object of class "thing" or "thingy".

A working Python equivalent of what I want to do is:

class thing:
    def __init__(self, number):
        self.number = number
    @property
    def number(self):
        return self._number
    @number.setter
    def number(self, value):
        if not isinstance(value, int):
            raise TypeError('"number" must be an int')
        self._number = value

class thingy:
    def __init__(self, text):
        self.text= text
    @property
    def text(self):
        return self._text
    @text.setter
    def text(self, value):
        if not isinstance(value, str):
            raise TypeError('"text" must be a str')
        self._text = value

class this_thing:
    def __init__(self, chosen_thing, name_of_the_thing):
        self.chosen_thing = chosen_thing
        self.name_of_the_thing = name_of_the_thing 
    @property
    def chosen_thing(self):
        return self._chosen_thing
    @chosen_thing.setter
    def chosen_thing(self, value):
        if (not isinstance(value, (thing, thingy))):
            raise TypeError('"chosen_thing" must be a thing or thingy')
        self._chosen_thing = value

    @property
    def name_of_the_thing(self):
        return self._name_of_the_thing
    @name_of_the_thing.setter
    def name_of_the_thing(self, value):
        if not isinstance(value, str):
            raise TypeError('"name_of_the_thing" must be a str')
        self._name_of_the_thing = value

some_thing = thing(10)
another_thing = thingy("10")
new_thing = this_thing(some_thing, "Some Thing")
another_new_thing = this_thing(another_thing, "Another Thing")

In C#, I have classes "Thing" and "Thingy" that work on their own. But I want to create a new class "ThisThing" which can take either an object of class "Thing" or "Thingy", but I'm not sure how to enable this action. IE declare / enable an object property to be of multiple types.

namespace TheManyThings
{
    class ThisThing
    {
        public Thing | Thingy NewThing { get; set; }
        public string NameOfTheThing { get; set; }

        public ThisThing(Thing | Thingy newThing, string nameOfTheThing)
        {
            NewThing = newThing;
            NameOfTheThing = nameOfTheThing
        }
    }
}

After playing around in C# it looks like the most workable solution was to just separate class "ThisThing" into two separate classes. It seems perhaps C# is not as flexible as Python with manipulating class types. Please of course comment, if you do know how to reproduce the above python code in C#. That would be handy to know.

namespace TheManyThings
{
    class ThisThing0
    {
        public Thing NewThing { get; set; }
        public string NameOfTheThing { get; set; }

        public ThisThing0(Thing newThing, string nameOfTheThing)
        {
            NewThing = newThing;
            NameOfTheThing = nameOfTheThing
        }
    }

    class ThisThing1
    {
        public Thingy NewThingy { get; set; }
        public string NameOfTheThing { get; set; }

        public ThisThing1(Thingy newThingy, string nameOfTheThing)
        {
            NewThingy = newThingy;
            NameOfTheThing = nameOfTheThing
        }
    }
}

In theory, you can replicate a dynamically typed system in C# by using dynamic , or a weak type like object .

But then, as you've done in Python, you will need to continually do type conformance checking at run time

As a result, you'll lose most of the benefits of a statically typed language like C#, and you'll also draw a lot of scorn from the SOLID OO community.

Here's a conversion of your original Python code using dynamic (DON'T do this)

public class Thing
{
    public Thing(int number) { Number = number; }
    public int Number { get; }
}

public class Thingy
{
    public Thingy(string text) { Text = text; }
    public string Text { get; }
}

public class ThisThing
{
    public ThisThing(dynamic chosenThing, string nameOfTheThing)
    {
        ChosenThing = chosenThing;
        NameOfTheThing = nameOfTheThing;
    }

    // Cache the accepted types
    private static readonly ICollection<Type> AcceptedTypes = new HashSet<Type> { typeof(Thing), typeof(Thingy) };
    private dynamic _chosenThing;
    public dynamic ChosenThing
    {
        get => _chosenThing;
        private set
        {
            if (!AcceptedTypes.Contains(value.GetType()))
            {
                throw new ArgumentException($"ChosenThing must be {string.Join(" or ", AcceptedTypes.Select(t => t.Name))}");
            }
            _chosenThing = value;
        }
    }

    public string NameOfTheThing { get; }
}

And as per your test cases, the following can be done:

var someThing = new Thing(10);
var anotherThing = new Thingy("10");
var newThing = new ThisThing(someThing, "Some Thing");
var anotherNewThing = new ThisThing(anotherThing, "Some Thing");

Console.WriteLine(newThing.ChosenThing.Number);
Console.WriteLine(anotherNewThing.ChosenThing.Text);

The problem with weak typing is that errors are only picked up at run time. The below will all get past the compiler (because ChosenThing is dynamic ) but will crash at run time.

var invalidThing = new ThisThing("Invalid for string types", "Invalid");
// newThing has a Number, not a Text property
Console.WriteLine(newThing.ChosenThing.Text);
// Vice Versa
Console.WriteLine(anotherNewThing.ChosenThing.Number);

Better approach using common interfaces

A far more acceptable OO approach is to have a common base class or interface to your 'acceptable' types, and then to use this instead. This way you will get compile-time type safety checking.

// Common interface
public interface IThing { }

public class Thing : IThing
{
    public Thing(int number) { Number = number; }
    public int Number { get; }
}

public class Thingy : IThing
{
    public Thingy(string text) { Text = text; }
    public string Text { get; }
}

Because of the common interface, IThing can now be used to constrain the permissable types passed to ThisThing (ie must conform to the IThing contract), and these type constraints are enforced at compile time:

public class ThisThing
{
    public ThisThing(IThing chosenThing, string nameOfTheThing)
    {
        ChosenThing = chosenThing;
        NameOfTheThing = nameOfTheThing;
    }

    public IThing ChosenThing { get; }

    public string NameOfTheThing { get; }
}

You would now expose any common functionality between Thing and Thingy via the IThing contract.

As it stands, the interface has no commonality, so you would need to downcast the IThing to one of the subclasses, which again, violates the SOLID Liskov Substitution Principal :

Console.WriteLine(((Thing)newThing.ChosenThing).Number);
Console.WriteLine(((Thingy)anotherNewThing.ChosenThing).Text);

So what you really want is to abstract commonality, eg

public interface IThing 
{ 
     int CalculateValue();
}

Both Thing and Thingy would now be forced to provide an implementation for this abstraction, and then consumers of the interface would now be able to use the interface safely without any further assumptions about the actual type of the concrete instance:

Console.WriteLine(anyIThing.CalculateValue());

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM