简体   繁体   中英

How to add a known type to a List<T>?

The overall goal here is like this: We have a lot of CSV files of various names and format stored in Azure blob storage. We need to convert them to lists.

I have an interface:

public interface IGpasData
{
    List<T> ConvertToList<T>(StreamReader reader);
}

And then here's an example of a class that Implements it:

public class GpasTableOfContent : IGpasData
{
    public string TocProp0 { get; set; }
    public string TocProp1 { get; set; }
    public string TocProp2 { get; set; }

    public List<T> ConvertToList<T>(StreamReader reader)
    {
        List<T> dataList = new List<T>();
        while (!reader.EndOfStream)
        {
            var lineItem = reader.ReadLine();
            GpasTableOfContent dataItem = new GpasTableOfContent
            {
                TocProp0 = lineItem.Split(',')[0],
                TocProp1 = lineItem.Split(',')[1],
                Type = lineItem.Split(',')[2]
            };
            dataList.Add(dataItem);
        }
        return dataList;
    }
}

To keep going with the example of the class above, there is a file called ToC.csv. In a class that is designed to convert THAT file into a list, I make this call:

List<GpasTableOfContent> gpasToCList = ConvertCloudFileToList<GpasTableOfContent>("ToC.csv", "MyModel");

Some other possible examples:

List<GpasFoo> gpasFooList = ConvertCloudFileToList<GpasFoo>("foo.csv", "MyModel");

List<GpasBar> gpasBarList = ConvertCloudFileToList<GpasBar>("bar.csv", "MyModel");

Here's ConvertCloudFileToList:

private List<T> ConvertCloudFileToList<T>(string fileName, string modelName)
    {
        // Get the .csv file from the InProgress Directory
        string filePath = $"{modelName}/{fileName}";
        CloudFile cloudFile = _inProgressDir.GetFileReference(filePath);

        List<T> dataList = new List<T>();

        // Does the file exist?
        if (!cloudFile.Exists())
            return dataList;

        using (StreamReader reader = new StreamReader(cloudFile.OpenRead()))
        {
            IGpasData gpasData = (IGpasData)Activator.CreateInstance<T>();
            dataList = gpasData.ConvertToList<T>(reader);
        }

        return dataList;
    }

And that brings us back to ConvertToList . The problem is here:

dataList.Add(dataItem);

Can not convert 'GpasFoo' to 'T'

Not sure how to work around this.

You can't do what you want here without providing some additional logic. The problem is that you have a string from reading the CSV file, and you want to convert it to a T, but there is no rule for converting a string into any arbitrary type.

One approach would be to change the method to also take a delegate Func that is used to convert each line into a T. Then if, for example, your data is guaranteed to consist of doubles, you could pass t => Double.Parse(t) for that argument. Of course, this approach requires that you change the signature of the interface method you are implementing.

If you are not able to change the signature of the interface method, then all I can suggest is trying to handle a pre-defined set of types and throwing an exception for other types.

As other have pointed out, this design is flawed:

 public interface IGpasData { List<T> ConvertToList<T>(StreamReader reader); } 

This contract says that an IGpasData should only know how deserialize anything. It doesn't make sense.

An IGpasData should know how to deserialize itself, and for this we would need a self-referencing interface:

public interface IGpasData<T> where T : IGpasData<T>
{
    List<T> ConvertToList(StreamReader reader);
}

public class GpasBar: IGpasData<GpasBar>
{
    public string MyPropertyA { get; set; }
    public int MyPropertyB { get; set; }

    public List<GpasBar> ConvertToList(StreamReader reader)
    {
        var results = new List<GpasBar>();
        while (!reader.EndOfStream)
        {
            var values = reader.ReadLine().Split(',');
            results.Add(new GpasBar()
            {
                PropertyA = values[0],
                PropertyB = int.Parse(values[1]),
            });
        }
        return results;
    }
}

Or, an IGpasData should know how to populate itself from an array of values:

public interface IGpasData
{
    void Populate(string[] values);
}
public class GpasBar
{
    public string MyPropertyA { get; set; }
    public int MyPropertyB { get; set; }

    public void Populate(string[] values)
    {
        MyPropertyA = values[0];
        MyPropertyB = int.Parse(values[1]);
    }
}

public static List<T> ConvertCloudFileToList<T>(string fileName, string modelName)
    where T : IGpasData, new()
{
    // ...
    using (StreamReader reader = new StreamReader(cloudFile.OpenRead()))
    {
        var results = new List<T>();
        while (!reader.EndOfStream)
        {
            var item = new T();
            item.Populate(reader.ReadLine().Split(','));

            results.Add(item);
        }
        return results;
    }
}

Using this 2nd approach, you can avoid duplicating the part about StreamReader and read lines.

Any object that is an IGpasData is expected to be able to produce a List of any given type when provided with a StreamReader . GpasTableOfContent does not fulfill this requirement, it can only produce a list of its own type.

However it doesn't seem reasonable to have one type of GpasData be responsible for converting everything so I'd suggest moving the Type argument from the ConvertToList method into the interface. This way subclasses will only be responsible for converting lists of a particular type.

public interface IGpasData<T>
{
    List<T> ConvertToList(StreamReader reader);
}

public class GpasTableOfContent : IGpasData<GpasTableOfContent>
{
    //...

    public List<GpasTableOfContent> ConvertToList(StreamReader reader)
    {
        //...
    }
}

On a side note, creating an empty table of contents and then using it to read from a stream and produce a list of the real table of contents seems very clunky to me. In my opinion, the behaviour of creating these content objects should be moved into its own class.

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