这两种方法有没有办法合并成一个方法,或者重载方法?

6

这是一个有点棘手的问题。也许有人的C#技能比我更强,因为我找不到解决方案。

我有一个方法,它接受一个参数,该参数包含枚举或指示枚举值的字符串,并返回该枚举的实例。它基本上是Enum.Parse的一种实现,但作为通用方法实现。我不知道为什么.NET Framework没有内置这个功能。

public static T Parse<T>(object value) where T : struct
{
   if (!typeof (T).IsEnum)
      throw new ArgumentException("T must be an Enum type.");

   if (value == null || value == DBNull.Value)
   {
      throw new ArgumentException("Cannot parse enum, value is null.");
   }

   if (value is String)
   {
      return (T)Enum.Parse(typeof(T), value.ToString());
   }

   return (T)Enum.ToObject(typeof(T), value);
}

现在,我可以做这样的事情:
MyEnum foo = Parse<MyEnum>(obj);

获取一个 MyEnum 实例。如果 obj 是 null,我会抛出一个异常。 然而,有时 objnull,我希望允许这种情况下的操作。在这种情况下,我想这样做:
MyEnum? foo = Parse<MyEnum?>(obj);

然而,尽管我很努力,但我无法找到一个方法来使它工作。首先,即使Nullable<MyEnum>是一个struct,它也无法用作Parse<T>的类型参数。我认为这与编译器对Nullable<>所做的所有魔法有关,因此我不会质疑它。

似乎你不能重载该方法并仅基于T上的约束进行区分。例如,如果我执行以下操作:

public static T Parse<T>(object value) where T : new()
{
  // This should be called if I pass in a Nullable, in theory
}

我会得到这个错误:已经声明了具有相同签名的成员 因此,我只剩下一种选择:实现一个专门针对可空类型设计的完全独立的方法:
public static T? ParseNullable<T>(object value) where T : struct
{
   if (!typeof (T).IsEnum)
      throw new ArgumentException("T must be an Enum type.");

   if (value == null || value == DBNull.Value)
      return null;

   if (value is String)
      return Enum.Parse(typeof (T), value.ToString()) as T?;

   return Enum.ToObject(typeof (T), value) as T?;
}

现在我可以这样调用:

MyEnum? foo = ParseNullable<T>(obj);
我的问题: 是否有一种方法可以将这两种方法合并成一个单独的方法,根据类型参数来正确处理,或者创建重载,其中一个重载用于 Nullable<> 类型参数的情况,另一个重载用于其他类型参数的情况?

你无法使用 Nullable<T> 版本重载该方法,因为你没有改变参数(因此不是重载);你只是改变了返回类型。 - pickypg
@pickypg - 是的,那就是我的问题;我希望编译器可以仅基于泛型类型约束来重载方法。也许在C# 6中会有这个功能呢。 - Mike Christensen
7个回答

3

需要在方法内添加几个额外的类型检查,并跳过泛型约束,但这是完全可能的:

public static T Parse<T>(object value)
{
    var isNullable = typeof(T).IsGenericType && typeof(T).GetGenericTypeDefinition() == typeof(Nullable<>);
    var itemType = isNullable ? typeof(T).GetGenericArguments()[0] : typeof(T);

    if (!itemType.IsEnum)
        throw new ArgumentException("T must be an Enum type or Nullable<> of Enum type.");

    if (value == null || value == DBNull.Value)
    {
        if (isNullable)
            return default(T);  // default(Nullable<>) is null

        throw new ArgumentException("Cannot parse enum, value is null.");
    }

    if (value is String)
    {
        return (T)Enum.Parse(itemType, value.ToString());
    }

    return (T)Enum.ToObject(itemType, value);
}

使用示例:

var items = new object[] { "A", "B", 0, 10, null, DBNull.Value };

var results = items.Select(x => new { x, e = Parse<Test?>(x) }).ToArray();

foreach (var r in results)
    Console.WriteLine("{0} - {1}", r.x, r.e.ToString());

打印

A - A
B - B
0 - A
10 - B
 -
 -

是的,有几个相同的答案,但我最喜欢这一个。代码易于阅读,并且底部有测试结果。 - Mike Christensen
最终我还是使用了一开始的方法,也就是两个名字不同的方法。虽然我真的希望编译器能够区分使用哪个方法,但我本来希望通过重载方法来实现这一点。以上方法的问题在于它将在运行时使用反射来访问类型数据,这违背了泛型方法的初衷。我正在使用这种方法在数据绑定代码中,可能需要处理成千上万行数据,在我的情况下,性能比花哨的使用语法更重要。谢谢! - Mike Christensen
1
@MikeChristensen 不用谢!顺便说一下,我真的很喜欢你决定做正确的事情并使用单独的方法 :) - MarcinJuraszek

2
为什么不要移除对T的限制,然后像这样做:
    public static T Parse<T>(Object value)
    {
        Boolean isNullable = typeof(T).GetGenericTypeDefinition() == typeof(Nullable<>);
        if (!isNullable && !typeof(T).IsEnum)
        {
            throw new ArgumentException();
        }

        if (value == null || value == DBNull.Value)
        {
            throw new ArgumentException();
        }

        if (!(value is String))
        {
            return (T) Enum.ToObject(typeof (T), value);
        }

        if (!isNullable)
        {
            return (T) Enum.Parse(typeof (T), value.ToString());
        }

        Type underlyingType = Nullable.GetUnderlyingType(typeof(T));
        try
        {
            return (T)Enum.Parse(underlyingType, value.ToString());
        }
        catch (ArgumentException)
        {
            return default(T);
        }
    }

那应该可以,如果不行,请告诉我。

1
创建一个类似于TryParse的方法,并处理返回值为false的情况,以便使用null值来执行所需操作。然后可以实现另一个方法来包装该调用,并在返回值为false时返回null。(此外,请确保使用Enum.IsDefined,因为枚举类型的任何值都可以分配给枚举,即使它没有被枚举定义)。
public static bool TryParseEnum<T>( object value, out T result ) where T : struct
{
    if( !typeof( T ).IsEnum )
        throw new ArgumentException( "T must be an Enum type." );

    if( value == null || value == DBNull.Value )
    {
        result = default( T );

        return false;
    }

    if( value is String )
    {
        return Enum.TryParse<T>( ( string )value, out result );
    }

    result = ( T )Enum.ToObject( typeof( T ), value );

    return Enum.IsDefined( typeof( T ), result );
}

public static Nullable<T> ParseEnum<T>( this object value ) where T: struct
{
    T retVal;

    if( !TryParseEnum( value, out retVal ) )
    {
        return null;
    }

    return new Nullable<T>( retVal );
}

使用方法:

EnumXyz? nullableEnumValue = ParseEnum<EnumXyz>( someObject );

1
我将提供另一种方法... 返回默认值。给枚举一个表示什么都没有的默认值是个好主意(如果你忘记初始化它等等)... 例如:
enum MyEnum {
    Nothing = 0,
    MeaningfulValue1,
    MeaningfulValue2
    // etc..
}

然后你的方法就变成了:


if (value == null || value == DBNull.Value)
    return default(T);

...以及调用该函数的位置:

var val = Parse<MyEnum>(obj);

if (val == MyEnum.Nothing)
    // it was null.

2
那么你永远不知道字符串是否包含默认值。而且,在.NET中,你不能不给enum一个默认值,你只能选择不为它的默认值指定任何标签。 - Jon Hanna
枚举的默认值是其基础类型的默认值,通常为0。 - Moho
@JonHanna 很容易添加。而且,如果不给一个标签,一旦被分配了值,检查该值就会变得很困难...这正是我明确给0一个标签的原因,以便它可以用作后备。 - Simon Whitehead
我的观点是Mike应该给枚举类型一个默认值,这个默认值应该有意义...即代表“无”。这样一来,像这样的通用代码就可以使用了。 - Simon Whitehead
给它贴上标签可以更轻松地检查其值;因为至少那么做会更容易区分默认的错误结果和默认的实际结果。尽管这很少发生。这大多数时候是无用的。 - Jon Hanna
显示剩余2条评论

1

由于您实际上没有通过更改返回类型进行重载,所以答案是您无法做到想要的。

我会添加一个重载,它接受一个单独的参数来确定参数的null性。

public static T Parse<T>(object value) where T : struct
{
    return (T)Parse<T>(value, false);
}

public static T? Parse<T>(object value, bool nullable) where T : struct
{
    T? enumValue = null;

    if ( ! typeof(T).IsEnum)
    {
        throw new ArgumentException("T must be an Enum type.");
    }
    else if (value == null || value == DBNull.Value)
    {
        // this is the key difference
        if ( ! nullable)
        {
            throw new ArgumentException("Cannot parse enum, value is null.");
        }
    }
    else if (value is string)
    {
        enumValue = (T)Enum.Parse(typeof(T), value.ToString());
    }
    else
    {
        enumValue = (T)Enum.ToObject(typeof(T), value);
    }

   return enumValue;
}

使用方法:

MyEnum value1 = Parse<MyEnum>("A");
// returns null
MyEnum? value2 = Parse<MyEnum>(null, true);
// throws exception
MyEnum? value2 = Parse<MyEnum>(null, false);

它不能返回null,因为它传递了false - pickypg
是的,有趣的想法... 不过似乎有点奇怪,我宁愿给这两个函数取不同的名字。 - Mike Christensen
你可以创建第二个方法,只作为一个传递,T? NullableParse<T>(object value) where T : struct { return Parse<T>(value, true); } - pickypg

1
如果你真的想使用一种方法,那么如何这样做呢?缺点是你必须移除where T : struct约束。如果你想保留约束,则将其分成两个方法是唯一的方法。
    public static T Parse<T>(object value)
    {
        Type underlyingType = Nullable.GetUnderlyingType(typeof(T));
        bool isNullable = underlyingType != null;

        if (!typeof(T).IsEnum && !isNullable)
            throw new ArgumentException("T must be an Enum type.");

        if (value == null || value == DBNull.Value)
        {
            if (isNullable)
                return default(T);

            throw new ArgumentNullException("value");
        }

        if (value is String)
            return (T)Enum.Parse(underlyingType ?? typeof(T), value.ToString());

        if (!value.GetType().IsValueType)
            throw new ArgumentException("value must be a primitive type", "value");

        return (T)Enum.ToObject(underlyingType ?? typeof(T), value);
    }

即使 TNullable<>,它仍会对 null 抛出异常。 - MarcinJuraszek
是的,我曾考虑过这种方法,但如果最终仍然要在运行时解析所有类型信息,那么它似乎真正错过了泛型方法的全部意义。不过,这种方法可能是我最好的选择。 - Mike Christensen

1
我认为对于你的问题,简短的答案是“不行”。在你提出问题的开头所提供的例子中,你想返回两种不同的返回类型,T和T?。这本身就需要使用不同名称的方法。
这里有一个链接,其中有一个关于泛型类型中可空性的很好的答案,可能有助于澄清问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接