简体   繁体   中英

How do I specify that a C# generic type must support Parse(string)?

I am trying to create a parser framework for XML strings containing SQL query results. The intent is to inherit from generic classes, which are instantiated with the column data types. The included code is for the single-column variety. There will be additional classes for two columns, etc.

I need to be able to specify that the generic type must support the Parse(string) method. How do I do this?

abstract class OneColumnParser<Col1>
{
    abstract string Column1;

    List<Col1> ParseQueryResult(string queryResult)
    {
        XmlDocument xDoc = new XmlDocument();
        xDoc.LoadXml(queryResult);
        List<Col1> results = new List<Col1>();

        foreach (XmlNode xNode in xDoc.GetElementsByTagName(Column1))
        {
            results.Add(Col1.Parse(xNode.InnerText));
        }
    }
}

When I compile the above, I get "'Col1' is a 'type parameter', which is not valid in the given context" on the results.Add() line, because I haven't specified that the type must support the method. But how?

One way is to define a parameterless constructor and an interface for your Col1 types:

interface IParseable
{
    void Parse(string text);
}

abstract class OneColumnParser<T> where T : IParseable, new
{
    abstract string Column1;

    List<T> ParseQueryResult<T>(string queryResult)
    {
        XmlDocument xDoc = new XmlDocument();
        xDoc.LoadXml(queryResult);
        var results = new List<T>();

        foreach (XmlNode xNode in xDoc.GetElementsByTagName(Column1))
        {
            var col = new T();
            col.Parse(xNode.InnerText);
            results.Add(col);
        }
    }
}

Because interfaces can't have static methods, you can't (directly) do what you're asking. Reflection is one way of solving the problem, but it's only verified at runtime, not enforced by the compiler. Eg

abstract class OneColumnParser<TCol>
{
    private static MethodInfo ParseInfo = typeof(TCol).GetMethod("Parse", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(string) }, null);
    abstract string Column1;

    static OneColumnParser()
    {
        if (typeof(TCol) != typeof(string) && (ParseInfo == null || !typeof(TCol).IsAssignableFrom(ParseInfo.ReturnType)))
            throw new InvalidOperationException("Invalid type, must contain public static TCol Parse(string)");
    }

    private static TCol Parse(string value)
    {
        if (typeof(TCol) == typeof(string))
            return (TCol)(object)value;
        else
            return (TCol)ParseInfo.Invoke(null, new[] { value });
    }

    public List<TCol> ParseQueryResult(string queryResult)
    {
        XmlDocument xDoc = new XmlDocument();
        xDoc.LoadXml(queryResult);
        List<TCol> results = new List<TCol>();

        foreach (XmlNode xNode in xDoc.GetElementsByTagName(Column1))
        {
            results.Add(Parse(xNode.InnerText));
        }

        return results;
    }
}

Unlike defining your own interface, this will work on existing types with Parse methods, such as int and DateTime . Update added code so that it will work on string as well.

Maybe this could help. When you declare generic function you can specify that generic must implement interafce. By this interface you can set Parse() support. For example:

public void SomeFunction<T>(T variable) where T : IDisposable
       {
           variable.Dispose();
       }

Not a direct answer to your question, but here is the code of a generic string unboxer class that I use to return typed values from a custom config section handler, might give you some ideas...

using System.ComponentModel;
using System.Data.SqlTypes;
using System.Threading;

public static class StringUnboxer<T> {
    private static readonly object _lock = new object();
    private static T m_convertedValue = default(T);

    public static T unBox(string value) {
        try {
            Monitor.Enter(_lock);
            // Test to see if value is valid to convert to supplied type
            if (canUnBox(value)) {
                // value is valid, return conversion
                return m_convertedValue;
            }
            else {
                // Conversion not possible with given string data, return default value for supplied type
                switch (typeof(T).ToString()) {
                    // In our case, if the supplied type is System.DateTime, we want to return 
                    // System.Data.SQLTypes.SQLDateTime.MinValue (01/01/1753) instead of
                    // System.DateTime.MinValue (01/01/0001) which is the normal default value
                    case "System.DateTime":
                        return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(SqlDateTime.MinValue.ToString());
                    // Return the .NET default value for all other types
                    default:
                        return default(T);
                }
            }
        }
        finally {
            Monitor.Exit(_lock);
        }
    }

    private static bool canUnBox(string value) {
        try {
            Monitor.Enter(_lock);
            m_convertedValue = (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(value);
            return true;
        }
        catch {
            return false;
        }
        finally {
            Monitor.Exit(_lock);
        }
    }
}

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