迭代变量的类型不同于集合?

时间:2010-09-14 13:11:21

标签: c# foreach nullable

我有一个可以为空的int集合。

为什么编译器允许迭代变量的类型为 int 而不是 int?

        List<int?> nullableInts = new List<int?>{1,2,3,null};
        List<int> normalInts = new List<int>();


        //Runtime exception when encounter null value
        //Why not compilation exception? 
        foreach (int i in nullableInts)
        {
         //do sth
        }

当然我应该注意我迭代的内容但是如果编译器训斥我​​会很好:)就像这里:

        foreach (bool i in collection)
        {
          // do sth 
        }

       //Error 1 Cannot convert type 'int' to 'bool'

3 个答案:

答案 0 :(得分:4)

因为C#编译器为你取消引用Nullable<T>

如果你写这段代码:

        var list = new List<int?>()
        {
            1,
            null
        };

        foreach (int? i in list)
        {
            if (!i.HasValue)
            {
                continue;
            }

            Console.WriteLine(i.GetType());
        }

        foreach (int i in list)
        {
            Console.WriteLine(i.GetType());
        }

C#编译器生成:

foreach (int? i in list)
{
    if (i.HasValue)
    {
        Console.WriteLine(i.GetType());
    }
}
foreach (int? CS$0$0000 in list)
{
    Console.WriteLine(CS$0$0000.Value.GetType());
}

请注意Nullable<int>.Value的显式解除引用。这证明了Nullable<T>结构在运行时中的根深蒂固。

答案 1 :(得分:3)

更新

好的,最初我说过“编译器将强制转换添加到foreach循环。”这不是严格准确:它不会始终添加强制转换。这是真正发生的事情。

首先,当你有这个foreach循环:

foreach (int x in collection)
{
}

...这是编译器创建的基本概要(伪C#):

int x;
[object] e;
try
{
    e = collection.GetEnumerator();
    while (e.MoveNext())
    {
        x = [cast if possible]e.Current;
    }
}
finally
{
    [dispose of e if necessary]
}

什么? 我听到你说。 [object]是什么意思?“

这就是我的意思。 foreach循环实际上需要无接口,这意味着它实际上有点神奇。它只要求枚举的对象类型公开GetEnumerator方法,而该方法必须提供某种类型的实例,该实例提供MoveNextCurrent属性。

所以我写了[object]因为e的类型不一定是IEnumerator<int>的实现,甚至是IEnumerator的实现 - 这也意味着它没有'必须实施IDisposable(因此[dispose if necessary]部分)。

我们为回答这个问题而关心的部分代码是我写[cast if possible]的部分。显然,由于编译器不需要实际的IEnumerator<T>IEnumerator实现,因此e.Current的类型不能假定为Tobject或任何内容之间。相反,编译器根据e.Current在编译时返回的类型确定GetEnumerator的类型。然后发生以下情况:

  1. 如果类型局部变量的类型(上例中为x),则使用直接赋值。
  2. 如果类型是 convertible 到局部变量的类型(我的意思是,从e.Current的类型到x的类型存在合法的演员表),插入一个演员。
  3. 否则,编译器将引发错误。
  4. 因此,在枚举List<int?>的情况下,我们进入第2步,编译器发现List<int?>.Enumerator类型的Current属性类型为int?,可以明确地转换为int

    因此该行可以编译为相当于:

    x = (int)e.Current;
    

    现在,explicit运算符对于Nullable<int>是什么样的?

    根据反射器:

    public static explicit operator T(T? value)
    {
        return value.Value;
    }
    

    因此,就我所知,the behavior described by Kent只是一个编译器优化:(int)e.Current显式转换是内联的。

    作为对你的问题的一般回答,我坚持认为编译器会在需要的foreach循环中插入强制转换。


    原始答案

    编译器会自动在foreach循环中的所需位置插入强制转换,原因很简单,因为在泛型之前没有IEnumerable<T>接口,只有IEnumerable *。 IEnumerable界面会显示IEnumeratorCurrent会提供对object类型的foreach属性的访问权限。

    因此,除非编译器为您执行了演员表,否则在过去,您使用object的唯一方法就是使用GetEnumerator类型的局部变量,这显然会“我们很糟糕。

    *实际上,foreach doesn't require any interface at all - 只有MoveNext方法以及带有Current和{{1}}的附带类型。

答案 2 :(得分:3)

您正在观察的行为是根据 8.8.4 C#语言规范的foreach语句部分。本节定义foreach语句的语义如下:

  

[...]上述步骤如果成功,则明确地生成集合类型C,枚举器类型E和元素类型T。表单的foreach声明

foreach (V v in x) embedded-statement
     

然后扩展为:

{
    E e = ((C)(x)).GetEnumerator();
    try {
        V v;
        while (e.MoveNext()) {
            v = (V)(T)e.Current;
            embedded-statement
        }
    }
    finally {
        // Dispose e
    }
}

根据规范中定义的规则,在您的示例中,集合类型将为List<int?>枚举器类型将为List<int?>.Enumerator元素类型为int?

如果您将此信息填写到上面的代码段中,您会看到通过调用Nullable<T> Explicit Conversion (Nullable<T> to T)int?显式转换为int。如Kent所述,这个显式强制转换运算符的实现只是返回Nullable<T>.Value属性。