Home > Software engineering >  C# - how to create a list that can contain one of three different types of models
C# - how to create a list that can contain one of three different types of models

Time:08-22

I have a project with a list that can be List<Fuel>, List<Trip>, or List<Purchase>. The models are very similar; the key difference is in how they are processed. Is there a way I can create one list object that can contain all three and be able to send the types to the right methods?

CodePudding user response:

There are multiple ways you can achieve that:

OPTION 1: List<object>

The easies one is List<object> and switch the type. IN c# you can do this:

var mylist = new List<object>(){ new Fuel(), new Trip(), new Purchase() };
foreach (var item in mylist)
{
  switch (item)
  {
    case Fuel item_as_fuel:
      item_as_fuel.Refuel(6);
      break;
    case Trip item_as_trip:
      item_as_trip.ToCity("Berlin");
      break;
    case Purchase pp:
      pp.SendPayment();
      break;
    default:
      throw new IvalidOperationException("invalid type");
  }
}

OPTION 2: interface

Another easy option is to implement an Interface Contrary to the abstract class in the previus answer, the interface will not beak your inherithance and will allow for easy expansion.

interface ISomeMeaningfulNameThatCorrelatesTheThreeClasses
{
  void PerformOrAnotherMeaningfulNameForYourSpecificCase(params object[] args);
}
// i will now call it IBase to simplify but you get the idea
class Fuel : IBase
{
  // i strongly suggest explicit implementations 
  // because it will only apply when used as interface
  void IBase.Perform(params object[] args)
  {
    //custom implementation
    this.Refuel((int)args[0]);
  }
}

Now, this is appropriate use of inheritance because interfaces are contracts, and this particolar explicit implementation of the contract ensures it's used only when explicitly casted to interface. ie:

IBase fuel = new Fuel();
fuel.Perform(6); // works

Fuel fuel = new Fuel();
fuel.Perform(6); // won't compile, Perform doesn't exist in Fuel

Option 3: union type

You could create a simple union type, having 3 nullable references to your types. This also could work as a substitute to the interface option, performing the correct operation in a method

public readonly struct FuelTripPurchaseUnion
{
  readonly Fuel? _f; //null
  readonly Trip? _t; //null
  readonly Purchase? _p; //null

  public FuelTripPurchaseUnion (object original)
  {/* assign to the correct variable */}
  public FuelTripPurchaseUnion (Fuel fuel = default, Trip trip = default, Purchase purchase = default)
  {/* assign to the correct variable */
   // used like FuelTripPurchaseUnion(fuel: thefuel);
  }

  public void Perform()
  {
    // you can put your logic here and call the correct one...
  }
  
  public object Value()
  {
    // ...or you can return the correct value
  }
}

Obviusly you could optimize the code further with advanced type annotations as said in the comments but it's more advanced stuff (new to me too, that's why i'm not putting it in the answer)

Option 3.1: external library

As said in the previus comments, another option is to use an external library giving you something like a OneOf<A,B,C> object instead of implementing one yourself.

but i won't suggest you use this one

CodePudding user response:

Quick-and-dirty tagged-union:

[StructLayout( LayoutKind.Explicit )]
public readonly struct Fuel_or_Trip_or_Purchase : IEquatable<Fuel_or_Trip_or_Purchase>, IEquatable<Fuel>, IEquatable<Trip>, IEquatable<Purchase>
{
    private const Byte TAG_DEFAULT  = 0;
    private const Byte TAG_FUEL     = 1;
    private const Byte TAG_TRIP     = 2;
    private const Byte TAG_PURCHASE = 3;
    
    public static implicit operator Fuel_or_Trip_or_Purchase( Fuel     fuel     ) => new Fuel_or_Trip_or_Purchase( fuel );
    public static implicit operator Fuel_or_Trip_or_Purchase( Trip     trip     ) => new Fuel_or_Trip_or_Purchase( trip );
    public static implicit operator Fuel_or_Trip_or_Purchase( Purchase purchase ) => new Fuel_or_Trip_or_Purchase( purchase );

    [FieldOffset(0)] private readonly Byte      tag;

    [FieldOffset(1)] private readonly Object    obj;
    [FieldOffset(1)] private readonly Fuel?     fuel;
    [FieldOffset(1)] private readonly Trip?     trip;
    [FieldOffset(1)] private readonly Purchase? purchase;

    public Fuel_or_Trip_or_Purchase( Fuel fuel )
    {
        this.obj      = fuel;
        this.purchase = null;
        this.trip     = null;
        
        this.fuel = fuel ?? throw new ArgumentNullException(nameof(fuel));
        this.tag  = TAG_FUEL;
    }

    public Fuel_or_Trip_or_Purchase( Trip trip )
    {
        this.obj      = trip;
        this.purchase = null;
        this.fuel     = null;
        
        this.trip = trip ?? throw new ArgumentNullException(nameof(trip));
        this.tag  = TAG_TRIP;
    }

    public Fuel_or_Trip_or_Purchase( Purchase purchase )
    {
        this.obj  = purchase;
        this.fuel = null;
        this.trip = null;
        
        this.purchase = purchase ?? throw new ArgumentNullException(nameof(purchase));
        this.tag      = TAG_PURCHASE;
    }

    public Boolean TryGetFuel( [NotNullWhen(true)] out Fuel? fuel )
    {
        fuel = ( this.tag == TAG_FUEL ) ? this.fuel : null;
        return fuel != null;
    }

    public Boolean TryGetTrip( [NotNullWhen(true)] out Trip? trip )
    {
        trip = ( this.tag == TAG_TRIP ) ? this.trip : null;
        return trip != null;
    }

    public Boolean TryGetPurchase( [NotNullWhen(true)] out Purchase? purchase )
    {
        purchase = ( this.tag == TAG_PURCHASE ) ? this.purchase : null;
        return purchase != null;
    }

    public override Int32 GetHashCode() => HashCode.Combine( this.tag, this.obj.GetHashCode() );

    public override Boolean Equals( Object? obj )
    {
        if( obj is null )
        {
            return false;// this.tag == TAG_DEFAULT;
        }
        else if( obj is Fuel_or_Trip_or_Purchase other )
        {
            return this.Equals( other: other );
        }
        else if( obj is Fuel f )
        {
            return this.Equals( f: f );
        }
        else if( obj is Trip t )
        {
            return this.Equals( t: t );
        }
        else if( obj is Purchase p )
        {
            return this.Equals( p: p );
        }
        else
        {
            return false;
        }
    }

    public Boolean Equals( Fuel_or_Trip_or_Purchase other ) => ( this.tag == other.tag ) && ( Object.ReferenceEquals( this.obj, other.obj ) );
    
    public Boolean Equals( Fuel?     f ) => ( this.tag == TAG_FUEL     ) && ( Object.ReferenceEquals( this.fuel    , f ) );
    public Boolean Equals( Trip?     t ) => ( this.tag == TAG_TRIP     ) && ( Object.ReferenceEquals( this.trip    , t ) );
    public Boolean Equals( Purchase? p ) => ( this.tag == TAG_PURCHASE ) && ( Object.ReferenceEquals( this.purchase, p ) );

    public TResult Match<TResult>(
        Func<Fuel,TResult>     whenFuel,
        Func<Trip,TResult>     whenTrip,
        Func<Purchase,TResult> whenPurchase,
    )
    {
        switch( this.tag )
        {
        case TAG_FUEL    : return whenFuel    ( this.fuel! );
        case TAG_TRIP    : return whenTrip    ( this.trip! );
        case TAG_PURCHASE: return whenPurchase( this.purchase! );
        default:
            throw new InvalidOperationException( "default(Fuel_or_Trip_or_Purchase)" );
        }
    }

    // Method forwarding:

    // Assuming Fuel, Trip and Purchase all have these members:

    public Decimal Cost => this.Match( f => f.Cost, t => t.Cost, p => p.Cost );
    public String  Name => this.Match( f => f.Name, t => t.Name, p => p.Name );
    // etc...
}

Used like so:


Fuel fuel0 = ..., fuel1 = ..., fuel2 = ...;
Trio trip0 = ..., trip1 = ..., trip2 = ...;
Purchase p = ...

List<Fuel_or_Trip_or_Purchase> list = new List<Fuel_or_Trip_or_Purchase>();

list.Add( fuel0 );
list.Add( trip0 );
list.Add( p );
list.Add( fuel1 );
list.Add( trip1 );

Decimal totalCost = list.Select( e => e.Cost ).Sum();
String  allNames  = String.Join( e => "\""   e.Name   "\"", separator: "\r\n" );

// Or:

Decimal totalCost = 0M;
StringBuilder allNamesSB = new StringBuilder();

foreach( var e in list )
{
    totalCost  = e.Cost;
    _ = allNamesSB.Append( '"' ).Append( e.Name ).Append( '"' ).AppendLine();
}

String allNames = allNamesSB.ToString();

CodePudding user response:

What you want is implemented through base class

public abstract class AbstractProcessor 
{
    public void CommonMethod() {} 
    public abstract void SpecificMethod() ;
} 

public class Fuel : AbstractProcessor 
{
    public override void SpecificMethod () 
    {
        // do fuel processing
    } 
} 
  •  Tags:  
  • c#
  • Related