简体   繁体   中英

How to combine two types of C# lists into one?

I have created two lists say X & Y. These two lists are of different types. (ie List<class_A> X & List<class_B> Y ).

Values in both these lists are different. But there is a DateTime field in both these lists.

I need to sort these lists according to the date field.

I have separate functions to print the details of list A and List B.

Suppose the sorted list looks like

  • tuple from list A,
  • tuple from list B,
  • tuple from list A,
  • tuple from list B,

My purpose is to loop through this list and call the appropriate function to display the details. ie if the tuple is from list A then call the function for printing list A's details and vice versa.

You can create an interface to host your common properties and functions, then join those lists after casting to this interface and sort it then:

interface ICommon
{
    DateTime DT { get; }
    void Print();
}
class ClassA : ICommon
{
    public DateTime DT { get; set; }
    public void Print() { Console.WriteLine("A " + DT); }
}
class ClassB : ICommon
{
    public DateTime DT { get; set; }
    public void Print() { Console.WriteLine("B " + DT); }
}

public static void Main()
{
    var listA = new List<ClassA> { new ClassA() { DT = DateTime.Now.AddDays(-1) }, new ClassA() { DT = DateTime.Now.AddDays(-3) }};
    var listB = new List<ClassB> { new ClassB() { DT = DateTime.Now.AddDays(-2) }, new ClassB() { DT = DateTime.Now.AddDays(-4) }};

    var orderedResult = listA.Cast<ICommon>()
                             .Concat(listB.Cast<ICommon>())
                             .OrderBy(q => q.DT);
    //or possibly even better:
    //var orderedResult = listA.Concat<ICommon>(listB).OrderBy(q => q.DT);
    foreach (var obj in orderedResult)
        obj.Print();
}

If you can change the definition of class A and class B then you should use one of the other answers here which tells you to add an interface to both those classes.

However, if you cannot change the definition of class A and class B (perhaps because they are defined in a library that you can't change) then you must take a different approach.

One solution which keeps things fairly neat is to introduce a wrapper class which holds either an instance of class A or class B (but not both).

Assume that that your classes look like this:

class A
{
    public DateTime Date;
    public double Value;
    public void Print() {}
}

class B
{
    public DateTime Date;
    public string Value;
    public void Print() { }
}

You can write a simple wrapper that looks like this:

class Wrapper
{
    readonly A a;
    readonly B b;

    public Wrapper(A a)
    {
        this.a = a;
    }

    public Wrapper(B b)
    {
        this.b = b;
    }

    public DateTime Date => a?.Date ?? b.Date;

    public void Print()
    {
        if (a != null)
            a.Print();
        else
            b.Print();
    }
}

Then given two lists:

var listA = new List<A>();
var listB = new List<B>();

You can create a sorted list of Wrapper objects:

var sorted = 
    listA.Select(a => new Wrapper(a))
    .Concat(listB.Select(b => new Wrapper(b)))
    .OrderBy(item => item.Date);

Then to call the appropriate Print() method for each item in the sorted list:

foreach (var item in sorted)
    item.Print();

If you make your Class A and Class B based upon a Base class , you can make them derive your DateTime property from Base class .

eg:

public class BaseClass
{
    public DateTime date {get; set; }
}

public class ClassA : BaseClass
{
    //... some other property's
}

public class ClassB : BaseClass
{
    //... some other property's
}

in your code:

List<BaseClass> a = new List<ClassA>();
List<BaseClass> b = new List<ClassB>();

And becose ClassA and ClassB are based upon BaseClass, they will have the same date property.

You could make a simple interface for the 2 classes you have:

public interface interfaceA {
   DateTime TimeStamp { get;set; }
}

And make sure both your classes implement that interface:

public class class_A : interfaceA {
    public DateTime TimeStamp { get; set; }
    //... Other properties
}

public class class_B : interfaceA {
    public DateTime TimeStamp { get; set; }
    //... Other properties
}

in your main function/method you create a new List that contains both object types and populate it - instead of filling one of your 2 lists you just fill this one list:

var myList = new List<interfaceA>();
var object = new class_A() { TimeStamp = DateTime.Now };
myList.Add(object); 

And then sort it:

 myList.Sort((a, b) => a.TimeStamp.CompareTo(b.TimeStamp))

I think something like this should work

No need to create new interfaces or classes. You can just loop over both lists at the same time and track the most recent date.

public class TypeA
{
    public DateTime date { get; set; }
    public void Print() { Console.WriteLine("TypeA: " + date.ToString()); }
}

public class TypeB
{
    public DateTime date { get; set; }
    public void Print() { Console.WriteLine("TypeB: " + date.ToString()); }
}

class Program
{
    static void Main(string[] args)
    {
        // setup
        var X = new List<TypeA>();
        var Y = new List<TypeB>();

        Random rnd = new Random();
        int imin, imax;

        imin = rnd.Next(3, 7);
        imax = rnd.Next(10, 20);

        for (int i = imin; i < imax; i++)
        {
            X.Add(new TypeA() { date = DateTime.Now.AddDays(-1 * rnd.Next(1, 1000)) });
        }

        imin = rnd.Next(3, 7);
        imax = rnd.Next(10, 20);

        for (int i = imin; i < imax; i++)
        {
            Y.Add(new TypeB() { date = DateTime.Now.AddDays(-1 * rnd.Next(1, 1000)) });
        }

        X = X.OrderBy(z => z.date).ToList();
        Y = Y.OrderBy(z => z.date).ToList();

        // begin print in order

        // index for X list
        int ix = 0;

        // index for Y list
        int iy = 0;

        // most recently printed date
        DateTime min = DateTime.MinValue;

        while (true)
        {
            if (ix < X.Count && X[ix].date >= min && (iy >= Y.Count || iy < Y.Count && X[ix].date <= Y[iy].date))
            {
                X[ix].Print();
                min = X[ix].date;
                ix++;
                continue;
            }

            if (iy < Y.Count && Y[iy].date >= min)
            {
                Y[iy].Print();
                min = Y[iy].date;
                iy++;
            }

            if (ix >= X.Count && iy >= Y.Count)
            {
                break;
            }
        }
    }
}

Sample output:

TypeB: 12/19/2013 3:44:44 PM
TypeB: 2/19/2014 3:44:44 PM
TypeB: 5/1/2014 3:44:44 PM
TypeA: 5/27/2014 3:44:44 PM
TypeA: 6/6/2014 3:44:44 PM
TypeA: 7/12/2014 3:44:44 PM
TypeA: 7/18/2014 3:44:44 PM
TypeB: 12/5/2014 3:44:44 PM
TypeB: 5/3/2015 3:44:44 PM
TypeB: 5/4/2015 3:44:44 PM
TypeB: 8/9/2015 3:44:44 PM
TypeA: 8/25/2015 3:44:44 PM
TypeA: 9/20/2015 3:44:44 PM
TypeB: 9/26/2015 3:44:44 PM
TypeA: 10/12/2015 3:44:44 PM
TypeA: 12/7/2015 3:44:44 PM
TypeB: 12/19/2015 3:44:44 PM
TypeA: 4/13/2016 3:44:44 PM
TypeA: 5/23/2016 3:44:44 PM
TypeB: 8/4/2016 3:44:44 PM
TypeB: 8/11/2016 3:44:44 PM

In the case where the original A and B types are incompatible and unchangeable (perhaps they're from some database where the types are unrelated), you can also use a projection using LINQ to merge them. This, in essence, creates a third class, albeit an anonymous one. In general, you should avoid this approach when you can modify the classes and use the base class or interface approach, but, as this is not always possible, below is a sample for using a LINQ projection. Note that the original types in this example have different names for the datetime which can be "merged" during the projection.

class Program
{
    static void Main(string[] args)
    {
        List<A> aList = new List<A> {new A {OtherAData = "First A", SomeDateTime = DateTime.Parse("12-11-1980")},
                                     new A {OtherAData = "Second A", SomeDateTime = DateTime.Parse("12-11-2000")} };
        List<B> bList = new List<B> {new B {OtherBData = "First B", SomeOtherDateTime = DateTime.Parse("12-11-1990")}, 
                                     new B {OtherBData = "Second B", SomeOtherDateTime = DateTime.Parse("12-11-2010")} };

        // create projections
        var unionableA = from a in aList
            select new {SortDateTime = a.SomeDateTime, AValue = a, BValue = (B) null};

        var unionableB = from b in bList
            select new {SortDateTime = b.SomeOtherDateTime, AValue = (A) null, BValue = b};

        // union the two projections and sort
        var union = unionableA.Union(unionableB).OrderBy(u => u.SortDateTime);

        foreach (var u in union)
        {
            if (u.AValue != null)
            {
                Console.WriteLine("A: {0}",u.AValue);
            }
            else if (u.BValue != null)
            {
                Console.WriteLine("B: {0}",u.BValue);
            }
        }

    }
}

public class A
{
    public DateTime SomeDateTime { get; set; }
    public string OtherAData { get; set; }
    public override string ToString()
    {
        return string.Format("SomeDateTime: {0}, OtherAData: {1}", SomeDateTime, OtherAData);
    }
}

public class B
{
    public DateTime SomeOtherDateTime { get; set; }
    public string OtherBData { get; set; }
    public override string ToString()
    {
        return string.Format("SomeOtherDateTime: {0}, OtherBData: {1}", SomeOtherDateTime, OtherBData);
    }
}

Sample Output:

 A: SomeDateTime: 12/11/1980 12:00:00 AM, OtherAData: First A
 B: SomeOtherDateTime: 12/11/1990 12:00:00 AM, OtherBData: First B
 A: SomeDateTime: 12/11/2000 12:00:00 AM, OtherAData: Second A
 B: SomeOtherDateTime: 12/11/2010 12:00:00 AM, OtherBData: Second B

Well, apart of interface solution, you can use abstract class and this class can contain default implementation of method Print :

abstract class AbstractClass
{
    protected string Details { get; set; }
    public DateTime DT { get; set; }

    private AbstractClass() { }

    protected AbstractClass(string details) { Details = details;}

    public virtual void Print()
    {
        Console.WriteLine($"{Details} {DT}");
    }
}

internal class ClassA : AbstractClass
{
    public ClassA() : base("A") { }
}

class ClassB : AbstractClass
{
    public ClassB() : base("B") { }
}

Usage :

var commonList = listA.Concat(listB.Cast<AbstractClass>()).OrderBy(e => e.DT).ToList();

commonList.ForEach(obj => obj.Print());

If you don't want to create a base type or interface you can do something like this:

new List<IEnumerable<dynamic>> { listA, listB }
    .SelectMany(x => x)
    .OrderBy(x => x.Date).ToList()
    .ForEach(x => Print(x));

You'll obviously need a Print() function, overloaded for class_A and class_B .

Making Print() a member function or extension method would make the above code even neater.

Create a third class having the properties of the other two. Then create a list of its objects and sort it by datetime.

I'd suggest a simpler way using an anonymous type with Action delegate. No need to change the definition of A and B, no need to introduce extra classes/interfaces.

class A
{
    public DateTime Date;
    public void Print() { Console.Write("A"); }
}

class B
{
    public DateTime Date;
    public void Print() { Console.Write("B"); }
}

class Program
{
    static void Main(string[] args)
    {
        // Generate sample data
        var r = new Random();

        var listA = Enumerable.Repeat(0, 10)
            .Select(_ => new A { Date = new DateTime(r.Next()) })
            .ToList();

        var listB = Enumerable.Repeat(0, 10)
            .Select(_ => new B { Date = new DateTime(r.Next()) })
            .ToList();


        // Combine the two lists into one of an anonymous type
        var combined = listA.Select(a => new { a.Date, Print = new Action(() => a.Print()) })
            .Concat(listB.Select(b => new { b.Date, Print = new Action(() => b.Print()) }))
            .OrderBy(c => c.Date)
            ;

        foreach (var item in combined)
        {
            item.Print();
            Console.Write(": " + item.Date);
            Console.WriteLine();
        }
    }
}

if I get the problem right then I would use a proxy class like this:

    public class Proxy
    {
        public object obj { get; set; }
        public DateTime date { get; set; }
    }

    var listAll = listA.Select(a => new Proxy { obj = a, date = a.date }).Union(listB.Select(b => new Proxy { obj = b, date = b.date })).OrderBy(d=>d.date).ToList();

    listAll.ForEach(c => 
       {
            if (c.obj.GetType() == typeof(A))
                printA(((A)c.obj));
            else if (c.obj.GetType() == typeof(B))
                printB(((B)c.obj));

       }

but I would rather let printing method to decide who is who

First, order the first by its property, then cast it to object, then concatenate it with the second list ordered by its property:

var lists = listA
.OrderBy(x => x.DateTimeProp)
.OfType<object>()
.Concat(
  listB
  .OrderBy(x => x.DateTimeProp)
  .OfType<object>()
);

After this, you can iterate through all items and act according to each item's type. Of course, this sorts each list independently. If you need to sort globally, you either need a base class or a shared interface that exposes the sortable property.

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