为什么在使用“var”时,Enum.GetValues()会返回名称?

29

有人能解释一下这个吗?

alt文本 http://www.deviantsart.com/upload/g4knqc.png

using System;

namespace TestEnum2342394834
{
    class Program
    {
        static void Main(string[] args)
        {
            //with "var"
            foreach (var value in Enum.GetValues(typeof(ReportStatus)))
            {
                Console.WriteLine(value);
            }

            //with "int"
            foreach (int value in Enum.GetValues(typeof(ReportStatus)))
            {
                Console.WriteLine(value);
            }

        }
    }

    public enum ReportStatus
    {
        Assigned = 1,
        Analyzed = 2,
        Written = 3,
        Reviewed = 4,
        Finished = 5
    }
}

当你将鼠标悬停在“var”上时,Visual Studio会显示什么类型? - ChrisF
没什么主意,但似乎非常有用! - sbenderli
@sbenderli - 我刚刚检查了一下,它是 System.Object,这可能在某种程度上解释了差异。 - ChrisF
@ChrisF,没错。如果您有一个值为枚举类型的“object”实例,则当Console.WriteLine()执行其.ToString()方法时,枚举值将返回其名称,例如“Assigned”(而不是其作为字符串的序数值,如“1”)。 - JMD
7个回答

43

Enum.GetValues 声明返回类型为 Array

它返回的数组包含实际值,作为 ReportStatus 值。
因此,var 关键字变成了 object,而 value 变量保存(装箱的)枚举值。
调用 Console.WriteLine 方法将解析为以 object 为参数的重载方法,并对其调用 ToString() 方法。对于枚举值,ToString() 返回名称。

当你迭代一个 int 类型时,编译器会隐式地将值强制转换为 int 类型,而 value 变量保存普通(非装箱)int 值。
因此,调用 Console.WriteLine 方法将解析为以 int 为参数的重载方法,并将其打印出来。

如果你将 int 更改为 DateTime (或任何其他类型),它仍然能够编译通过,但在运行时会抛出 InvalidCastException 异常。


那么底层返回的IEnumerable依赖于隐式转换,是吗? - Andreas
3
好的解释,点赞。请注意,如果枚举的基础类型是其他数字类型(例如uint或short),第二个foreach循环也会失败。 - Thomas Levesque
@Andreas,第一个循环中没有进行任何转换,第二个循环只允许继承转换。也就是说,没有隐式或显式定义的强制类型转换。 - Dykam

5
根据MSDN文档,接受一个object参数的Console.WriteLine重载会在其参数上调用ToString方法。
当你使用foreach (var value in ...)循环时,你的value变量被视为object类型(因为SLaks指出Enum.GetValues返回的是未经类型化的Array),所以你的Console.WriteLine会调用object.ToString方法,而这个方法被System.Enum.ToString重写了。该方法返回枚举的名称
当你使用foreach (int value in ...)循环时,你将枚举值转换为int值(而不是object),因此Console.WriteLine会调用System.Int32.ToString方法。

是的,foreach(ReportStatus)编译并打印名称。 - Hans Kesting

3

FWIW,以下是通过Reflector分解的Enum.GetValues()代码:

[ComVisible(true)]
public static Array GetValues(Type enumType)
{
    if (enumType == null)
    {
        throw new ArgumentNullException("enumType");
    }
    if (!(enumType is RuntimeType))
    {
        throw new ArgumentException(Environment.GetResourceString("Arg_MustBeType"), "enumType");
    }
    if (!enumType.IsEnum)
    {
        throw new ArgumentException(Environment.GetResourceString("Arg_MustBeEnum"), "enumType");
    }
    ulong[] values = GetHashEntry(enumType).values;
    Array array = Array.CreateInstance(enumType, values.Length);
    for (int i = 0; i < values.Length; i++)
    {
        object obj2 = ToObject(enumType, values[i]);
        array.SetValue(obj2, i);
    }
    return array;
}

看起来大家关于 var 是一个 object 并且调用 object.ToString() 返回名称的说法是正确的...


现在有些人会因为你发布了反汇编代码而对你进行挑衅...无论如何我还是要点赞。 - Mau
我的立场是,如果微软反对反汇编,他们可以轻松地混淆库... - Tim Coker

2

当您使用Console.WriteLine时,每个元素都会被隐式调用ToString()。

而且,当您使用显式类型int时,它将将其转换为int,然后再ToString()。

第一个是Enum值ToString()的结果。


1

编辑:添加了一些探索数组迭代的许多(也许全部?)可能方法的示例代码。

枚举类型默认情况下被认为是从 int 派生的。您可以选择将其派生自其他整数类型,例如 byte、short、long 等。

在两种情况下,对 Enum.GetValues 的调用都会返回一个 ReportStatus 对象数组。

在第一个循环中使用 var 关键字告诉编译器使用指定类型的数组,即 ReportStatus,来确定值变量的类型。枚举的 ToString 实现是返回枚举条目的名称,而不是它表示的整数值,这就是为什么第一个循环输出名称的原因。

在第二个循环中使用 int 变量会导致由 Enum.GetValues 返回的值从 ReportStatus 隐式转换为 int。在 int 上调用 ToString 当然会返回表示整数值的字符串。隐式转换是导致行为差异的原因。

更新:正如其他人指出的那样,Enum.GetValues 函数返回一个类型为 Array 的对象,并且因此它是 Object 类型的可枚举对象,而不是 ReportStatus 类型的可枚举对象。

无论如何,无论是迭代Array还是ReportStatus[],最终结果都是相同的:

class Program
{
    enum ReportStatus
    {
        Assigned = 1,
        Analyzed = 2,
        Written = 3,
        Reviewed = 4,
        Finished = 5,
    }

    static void Main(string[] args)
    {
        WriteValues(Enum.GetValues(typeof(ReportStatus)));

        ReportStatus[] values = new ReportStatus[] {
            ReportStatus.Assigned,
            ReportStatus.Analyzed,
            ReportStatus.Written,
            ReportStatus.Reviewed,
            ReportStatus.Finished,
        };

        WriteValues(values);
    }

    static void WriteValues(Array values)
    {
        foreach (var value in values)
        {
            Console.WriteLine(value);
        }

        foreach (int value in values)
        {
            Console.WriteLine(value);
        }
    }

    static void WriteValues(ReportStatus[] values)
    {
        foreach (var value in values)
        {
            Console.WriteLine(value);
        }

        foreach (int value in values)
        {
            Console.WriteLine(value);
        }
    }
}

为了增加一些额外的乐趣,我在下面添加了一些代码,演示了使用foreach循环迭代指定数组的几种不同方法,包括详细描述每种情况下发生的情况的注释。

class Program
{
    enum ReportStatus
    {
        Assigned = 1,
        Analyzed = 2,
        Written = 3,
        Reviewed = 4,
        Finished = 5,
    }

    static void Main(string[] args)
    {
        Array values = Enum.GetValues(typeof(ReportStatus));

        Console.WriteLine("Type of array: {0}", values.GetType().FullName);

        // Case 1: iterating over values as System.Array, loop variable is of type System.Object
        // The foreach loop uses an IEnumerator obtained from System.Array.
        // The IEnumerator's Current property uses the System.Array.GetValue method to retrieve the current value, which uses the TypedReference.InternalToObject function.
        // The value variable is passed to Console.WriteLine(System.Object).
        // Summary: 0 box operations, 0 unbox operations, 1 usage of TypedReference
        Console.WriteLine("foreach (object value in values)");
        foreach (object value in values)
        {
            Console.WriteLine(value);
        }

        // Case 2: iterating over values as System.Array, loop variable is of type ReportStatus
        // The foreach loop uses an IEnumerator obtained from System.Array.
        // The IEnumerator's Current property uses the System.Array.GetValue method to retrieve the current value, which uses the TypedReference.InternalToObject function.
        // The current value is immediatly unboxed as ReportStatus to be assigned to the loop variable, value.
        // The value variable is then boxed again so that it can be passed to Console.WriteLine(System.Object).
        // Summary: 1 box operation, 1 unbox operation, 1 usage of TypedReference
        Console.WriteLine("foreach (ReportStatus value in values)");
        foreach (ReportStatus value in values)
        {
            Console.WriteLine(value);
        }

        // Case 3: iterating over values as System.Array, loop variable is of type System.Int32.
        // The foreach loop uses an IEnumerator obtained from System.Array.
        // The IEnumerator's Current property uses the System.Array.GetValue method to retrieve the current value, which uses the TypedReference.InternalToObject function.
        // The current value is immediatly unboxed as System.Int32 to be assigned to the loop variable, value.
        // The value variable is passed to Console.WriteLine(System.Int32).
        // Summary: 0 box operations, 1 unbox operation, 1 usage of TypedReference
        Console.WriteLine("foreach (int value in values)");
        foreach (int value in values)
        {
            Console.WriteLine(value);
        }

        // Case 4: iterating over values as ReportStatus[], loop variable is of type System.Object.
        // The foreach loop is compiled as a simple for loop; it does not use an enumerator.
        // On each iteration, the current element of the array is assigned to the loop variable, value.
        // At that time, the current ReportStatus value is boxed as System.Object.
        // The value variable is passed to Console.WriteLine(System.Object).
        // Summary: 1 box operation, 0 unbox operations
        Console.WriteLine("foreach (object value in (ReportStatus[])values)");
        foreach (object value in (ReportStatus[])values)
        {
            Console.WriteLine(value);
        }

        // Case 5: iterating over values as ReportStatus[], loop variable is of type ReportStatus.
        // The foreach loop is compiled as a simple for loop; it does not use an enumerator.
        // On each iteration, the current element of the array is assigned to the loop variable, value.
        // The value variable is then boxed so that it can be passed to Console.WriteLine(System.Object).
        // Summary: 1 box operation, 0 unbox operations
        Console.WriteLine("foreach (ReportStatus value in (ReportStatus[])values)");
        foreach (ReportStatus value in (ReportStatus[])values)
        {
            Console.WriteLine(value);
        }

        // Case 6: iterating over values as ReportStatus[], loop variable is of type System.Int32.
        // The foreach loop is compiled as a simple for loop; it does not use an enumerator.
        // On each iteration, the current element of the array is assigned to the loop variable, value.
        // The value variable is passed to Console.WriteLine(System.Int32).
        // Summary: 0 box operations, 0 unbox operations
        Console.WriteLine("foreach (int value in (ReportStatus[])values)");
        foreach (int value in (ReportStatus[])values)
        {
            Console.WriteLine(value);
        }

        // Case 7: The compiler evaluates var to System.Object.  This is equivalent to case #1.
        Console.WriteLine("foreach (var value in values)");
        foreach (var value in values)
        {
            Console.WriteLine(value);
        }

        // Case 8: The compiler evaluates var to ReportStatus.  This is equivalent to case #5.
        Console.WriteLine("foreach (var value in (ReportStatus[])values)");
        foreach (var value in (ReportStatus[])values)
        {
            Console.WriteLine(value);
        }
    }
}

-- 在上面的示例中更新了我的注释;在仔细检查后,我发现 System.Array.GetValue 方法实际上使用 TypedReference 类来提取数组元素并将其作为 System.Object 返回。我最初写道那里发生了装箱操作,但从技术上讲并非如此。我不确定装箱操作与调用 TypedReference.InternalToObject 的比较是什么;我认为这取决于 CLR 实现。无论如何,我相信现在的细节或多或少是正确的。


1
虽然我关于“var”变成对象的说法是错误的,但是“value”的值变量作为一个装箱枚举值的对象其实跟问题为何行为不同无关。我已经添加了一个代码示例,证明了无论“var”是对象还是“ReportStatus”,行为都是相同的。 - Dr. Wily's Apprentice

0

枚举类型不同于整数。在你的示例中,var 并不是 int,而是枚举类型。如果你使用了枚举类型本身,你将得到相同的输出。

枚举类型在打印时输出名称,而不是值。


2
接近了,但是 var 并没有评估为枚举类型;实际上,它评估为 object(请参见 SLaks 的答案)。 - Dan Tao
2
从技术上讲,正如SLaks所指出的那样,编译器将var评估为对象,但在运行时,该值是枚举类型(尽管已装箱)。该值被装箱的事实与问题所要求的行为无关。 - Dr. Wily's Apprentice

0

var value 实际上是一个枚举值(ReportStatus 类型),因此您会看到 enumValue.ToString() 的标准行为 - 它的名称。

编辑:
当您执行 Console.WriteLine(value.GetType()) 时,您将看到它确实是一个 'ReportStatus',尽管它被装箱在一个普通的 Object 中。


1
错误。这里的“var”变成了“object”。 - SLaks
这个答案实际上并不是错误的,尽管不如SLaks的答案详细。 value变量实际上是ReportStatus类型,只是编译器将var评估为object,因为它无法确定Enum.GetValues返回的特定类型的数组。 然而,在运行时评估(value is ReportStatus)的结果是true。 无论value变量是否是包含ReportStatus的对象,还是实际上是ReportStatus,对于所询问的行为来说实际上都是不相关的。 - Dr. Wily's Apprentice

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