How do I properly work with calling methods on related but different classes in C#
Asked Answered
K

4

11

To be honest I wasn't sure how to word this question so forgive me if the actual question isn't what you were expecting based on the title. C# is the first statically typed language I've ever programmed in and that aspect of it has been an absolute headache for me so far. I'm fairly sure I just don't have a good handle on the core ideas surrounding how to design a system in a statically typed manner.

Here's a rough idea of what I'm trying to do. Suppose I have a hierarchy of classes like so:

abstract class DataMold<T>
{
    public abstract T Result { get; }
}

class TextMold : DataMold<string>
{
  public string Result => "ABC";
}  

class NumberMold : DataMold<int>
{
   public int Result => 123
}

Now suppose I want to make a list of item where the items can be any kind of mold and I can get the Result property of each item in a foreach loop like so:

List<DataMold<T>> molds = new List<DataMold<T>>();
molds.Add(new TextMold());
molds.Add(new NumberMold());

foreach (DataMold<T> mold in molds)
    Console.WriteLine(mold.Result);

As you probably already know, that doesn't work. From what I've read in my searches, it has to do with the fact that I can't declare the List to be of type DataMold<T>. What is the correct way to go about something like this?

Kirt answered 22/6, 2018 at 22:4 Comment(2)
interface IDataMold<T> would work as a List<IDataMold<T>>`, if an interface suites your needs.Cohbert
normally this would look like class TextMold : DataMold<string> { public string Result { get; set; } } OR it would just have a getter: class TextMold : DataMold<string> { public string Result { get; private set; } }Vikki
G
11

The short answer: You can't.

One of the things that is counterintuitive about generic types is that they are not related. A List<int>, for example, has no relationship whatsoever to a List<string>. They do not inherit from each other, and you can't cast one to the other.

You can declare a covariance relationship, which looks a lot like an inheritance relationship, but not between an int and a string as you have declared, since one is a value type and one is a reference type.

Your only alternative is to add another interface that they have in common, like this:

interface IDataMold
{
}

abstract class DataMold<T> : IDataMold
{
    public abstract T Result { get; }
}

Now you can store all of your molds in a List<IDataMold>. However, the interface has no properties, so you'd have a heckuva time getting anything out of it. You could add some properties, but they would not be type-specific, as IDataMold has no generic type parameter. But you could add a common property

interface IDataMold
{
    string ResultString { get; }
}

...and implement it:

abstract class DataMold<T>
{
    public abstract T Result { get; }
    public string ResultString => Result.ToString();
}

But if your only need is to display a string equivalent for each item, you can just override ToString() instead:

class TextMold : DataMold<string>
{
    public string Result => "ABC";
    public override string ToString() => Result.ToString();
}

Now you can do this:

List<IDataMold> molds = new List<IDataMold>();
molds.Add(new TextMold());
molds.Add(new NumberMold());

foreach (var mold in molds)
{
    Console.WriteLine(mold.ToString());
}
Gyniatrics answered 22/6, 2018 at 22:17 Comment(0)
P
4

You're looking for covariance. See the out keyword before T generic type parameter:

// Covariance and contravariance are only possible for
// interface and delegate generic params
public interface IDataMold<out T> 
{
   T Result { get; }
}

abstract class DataMold<T> : IDataMold<T>
{
  public abstract T Result { get; }
}

class StringMold : DataMold<string> {}

class Whatever {}

class WhateverMold : DataMold<Whatever> {}

Now inherit DataMold<T> and create a List<IDataMold<object>>:

var molds = new List<IDataMold<object>>();
molds.Add(new StringMold());
molds.Add(new WhateverMold());

BTW, you can't use covariance when it comes to cast IDataMold<int> to IDataMold<object>. Instead of repeating what's been already explained, please see this other Q&A: Why covariance and contravariance do not support value type

If you're really forced to implement IDataMold<int>, that list may be of type object:

var molds = new List<object>();
molds.add(new TextMold());
molds.add(new NumberMold());

And you may use Enumerable.OfType<T> to get subsets of molds:

var numberMolds = molds.OfType<IDataMold<int>>();
var textMolds = molds.OfType<IDataMold<string>>();

Also, you may create two lists:

var numberMolds = new List<IDataMold<int>>();
var textMolds = new List<IDataMold<string>>();

So you might mix them later as an IEnumerable<object> if you need to:

var allMolds = numberMolds.Cast<object>().Union(textMolds.Cast<object>());
Panatella answered 22/6, 2018 at 22:31 Comment(0)
E
2

You could use a visitor pattern:

Add a visitor interface that accepts all your types, and implement a visitor that performs the action you want to apply to all DataMolds:

interface IDataMoldVisitor
{  void visit(DataMold<string> dataMold);
   void visit(DataMold<int> dataMold);
}

// Console.WriteLine for all
class DataMoldConsoleWriter : IDataMoldVisitor
{  public void visit(DataMold<string> dataMold)
   {  Console.WriteLine(dataMold.Result);
   }
   public void visit(DataMold<int> dataMold)
   {  Console.WriteLine(dataMold.Result);
   }
}

Add an acceptor interface that your list can hold and have your DataMold classes implement it:

interface IDataMoldAcceptor
{  void accept(IDataMoldVisitor visitor);
}

abstract class DataMold<T> : IDataMoldAcceptor
{  public abstract T Result { get; }
   public abstract void accept(IDataMoldVisitor visitor);
}

class TextMold : DataMold<string>
{  public string Result => "ABC";
   public override void accept(IDataMoldVisitor visitor)
   {  visitor.visit(this);
   }
}  

class NumberMold : DataMold<int>
{  public int Result => 123;
   public override void accept(IDataMoldVisitor visitor)
   {  visitor.visit(this);
   }
}

And finally, execute it with:

// List now holds acceptors
List<IDataMoldAcceptor> molds = new List<IDataMoldAcceptor>();
molds.Add(new TextMold());
molds.Add(new NumberMold());

// Construct the action you want to perform
DataMoldConsoleWriter consoleWriter = new DataMoldConsoleWriter();
// ..and execute for each mold
foreach (IDataMoldAcceptor mold in molds)
    mold.accept(consoleWriter);

Output is:

ABC
123
Enchiridion answered 23/6, 2018 at 3:7 Comment(0)
P
1

dynamic

This can be done with the dynamic keyword, at the cost of performance and type safety.

var molds = new List<object>();     // System.Object is the most derived common base type.
molds.Add(new TextMold());
molds.Add(new NumberMold());

foreach (dynamic mold in molds)
    Console.WriteLine(mold.Result);

Now that mold is dynamic, C#'ll check what mold's type is at run-time and then figure out what .Result means from there.

Polysyllabic answered 23/6, 2018 at 7:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.