C#集合-列举(Enumeration)

本文深入探讨了C#中的IEnumerable与IEnumerator接口,解释了它们在集合遍历中的作用,包括非泛型与泛型版本的区别,以及如何通过yield return语句实现集合遍历。同时,文章还对比了IEnumerator与Iterator的概念,帮助读者理解两者之间的差异。

在计算机这个范畴内存在许多种类的集合,从简单的数据结构比如数组、链表,到复杂的数据结构比如红黑树,哈希表。尽管这些数据结构的内部实现和外部特征大相径庭,但是遍历集合的内容确是一个共同的需求。.NET Framework通过IEnumerable和IEnumerator接口实现遍历集合功能。

Non-GenericGeneric备注
IEnumeratorIEnumerator<T> 
IEnumerableIEnumerable<T>仅可遍历
ICollectionICollection<T>遍历,可统计集合元素
IDictionary
IList
IDictionary<TKey,TValue>
IList<T>
拥有更过的功能

 

IEnumerable与IEnumerator

IEnumerator接口定义了遍历协议--在这个协议中,集合中的元素使用向前的方式进行遍历。它的声明如下:

public interface IEnumerator
{   
    bool MoveNext();
   
    Object Current { get; }

    void Reset();
}

MoveNext将当前元素或指针移动到下一个位置,如果下一个位置没有元素那么返回false。Current返回在当前值位置的元素。在获取集合的第一个元素之前,必须调用MoveNext方法--这对于空集合同样适用。Reset方法,这移动到初始位置,从而允许集合可以再次遍历。Reset更过多是为COM互操作而设计:应该尽量直接避免调用此方法,因为它并没有得到普遍的支持(直接调用此方法是不必要的,因为创建一个新的列举实例更容易)。

集合一般都不实现列举器,相反,它们通过IEnurable接口提供列举器

public interface IEnumerable
{   
    IEnumerator GetEnumerator();
}

通过定义一个单一返回列举器的方法,IEnumerable接口提供了更多的灵活性,从而各个实现类的遍历集合的逻辑可以各部相同。这也就意味着每个集合的使用者都可以创建自己的方法遍历集合而不会相互影响。IEnumerable可以被视作IEnumeratorProvider,它是所有集合类都必须实现的一个接口。

下面的代码演示了如何使用IEnumerable和IEnumerator:

string s = "Hello";

// IEnumerator
IEnumerator rator = s.GetEnumerator();
while (rator.MoveNext())
    Console.Write(rator.Current + ".");

Console.WriteLine();

// IEnumerable
foreach (char c in s)
    Console.Write(c + ".");

一般地,很少调用GetEnumerator方法得到IEnumerator接口,这是由于C#提供了foreach语法(foreach语法编译后,会自动调用GetEnumerator从而遍历集合),这使得代码变得更简洁。

 

IEnumerable<T>与IEnumerator<T>

IEnumerator和IEnumerable对应的Generic接口定义如下:

public interface IEnumerator<out T> : IDisposable, IEnumerator
{    
       new T Current {
        get; 
    }
}
public interface IEnumerable<out T> : IEnumerable { new IEnumerator<T> GetEnumerator(); }

Generic的Current和GetEnumerator,增加了接口IEnumerable<T>与IEnumerator<T>的类型安全性,避免了对值类型进行装箱操作,对于集合的使用者更加便利。请注意,数字类型默认实现了IEnumerable<T>接口。

正是由于实现了类型安全的接口,方法Test2(arr)在编译时就会报错:

static void Main(string[] args)
{
    char[] arr = new char[] { '1', '2', '3' };
    Test1(arr);  // ok
    Test2(arr); // complie-error: cannot convert from char[] to IEnumerable[]

    Console.ReadLine();
}

static void Test1(IEnumerable numbers)
{
    foreach (object i in numbers)
        Console.Write(i + ",");
}

static void Test2(IEnumerable<int> numbers)
{
    foreach (object i in numbers)
        Console.Write(i + ",");
}

请注意,Array默认实现了IEnumerable<T>接口,那么它同时必然实现了IEnumerable接口。虽然char[]不能转换成IEnumrable<int>,但是却可以转换成IEnumeable,所以Test1可以通过编译,而Test2不能通过编译(类型转化失败错误)

对于集合类,对外暴露IEnumerable<T>是标准做法;并需要显示地实现IEnumerable接口,从而隐藏非Generic的IEnumerable。此时,你再调用GetEnumerator,将得到IEnumerator<T>。但有时候,为了兼容非Generic的集合,我们可以不遵守这个规则。最好的例子就是数组集合,数组必须返回非generic的IEnumerator以避免与早期的代码冲突。在这种情况下,为了获取IEnumerator<T>,就必须先把数组显示地转化为Generic接口,然后再获取:

char[] arr = new char[] { '1', '2', '3' };
var rator = ((IEnumerable<char>)arr).GetEnumerator();

幸运的是,你很少需要编写这样的代码,这就要归功于foreach语句。

 

IEnumerable<T>和IDisposable

IEnumerator<T>继承了IDisposable。这就允许列举器可以拥有资源的引用比如数据库连接,从而确保在遍历完成后释放这些资源。foreach会语句会识别这个特性,比如,下面的foreach语句

IList<char> chars =new List<char>(){'a', 'b', 'c'};
 foreach (char c in chars)
     Console.Write(c);

编译后的代码为:

.method private hidebysig static void  Main(string[] args) cil managed
{
 ......
  IL_0026:  callvirt   instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<char>::GetEnumerator()
  IL_002b:  stloc.3
  .try
  {
   .......
System.Collections.Generic.IEnumerator`1<char>::get_Current()
   ......
    IL_0036:  call       void [mscorlib]System.Console::Write(char)
    ......
    IL_003d:  callvirt   instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
  ......
  }  // end .try
  finally
  {
    ......
    IL_0055:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
   ......
  }  // end handler
  ......
} // end of method Program::Main

因此,如果实现了IEnumable<T>接口,执行foreach时,会转化成调用GetEnumerator<T>, 在遍历完成之后,释放IEnumerator<T>。

 

实现列举接口

当满足下面的一个或多个条件时,需要实现IEnumerable或IEnumerable<T>

  1. 为了支持foreach语句
  2. 为了实现除了标准集合之外的集合都是可互操作的
  3. 为了满足一个复杂集合接口
  4. 为了支持集合初始化

而实现IEnumerable/IEnumerable<T>,你必须提供一个列举器,你可以通过下面三种方式实现

  • 如果类包含了另外集合,那么需要返回所包含集合的列举器
  • 在迭遍历内部使用yield return
  • 实例化IEnumerator/IEnumerator<T>的实现

1)实例IEnumerator/IEnumerator<T>

返回另外一个集合的列举器就是调用内部集合的GetEnumerator。但是,这只发生在简单的场景中,在这样的场景中,内部集合中的元素已经满足需要。另外一种更为灵活的方式是通过yield return语句生成一个迭代器。迭代器(iteraotr)是C#语言特性,该特性用于辅助生产集合,同样地foreach可与用于iterator以遍历集合。一个迭代器自动处理IEnumerable和IEnumerator的实现。下面是一个简单的例子

internal class MyCollection : IEnumerable
{
    int[] data ={ 1, 2, 3 };

    public IEnumerator GetEnumerator()
    {
        foreach (int i in data)
            yield return i;
    }
}

请注意,GetEnumerator根本就没有返回一个列举器。依赖于解析yield return后的语句,编译器编写了一个隐藏的内嵌列举器类,然后重构  GetEnumerator实现实例化,最后返回该类。迭代不仅功能强大而且简单。

通过IL代码,我们可以看到确实生产了一个内嵌的列举器类

image 

 

我们在上面代码的基础上,对MyCollecton做些许修改,使其不仅仅实现IEnumerable,还实现IEnumerable<T>

internal class MyCollection : IEnumerable<int>
{
    int[] data ={ 1, 2, 3 };        

    public IEnumerator<int> GetEnumerator()
    {
        foreach (int i in data)
            yield return i;
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

因为IEnumerable<T>继承了IEnumerable,因此我们必须实现generic的GetEnumerator和非generic的GetEnumerator。按照标准的做法,我们已经实现了Generic的GetEnumerator。因此对于非Generic的GetEnumerator,我们直接调用Generic的GetEnumerator即可,这是因为IEnumerable<T>继承了IEnumerbale。

对应的IL代码如下:(请注意编译器实现的IEnumerator<Int32>接口,而不再是IEnumerator<Object>接口

image

2)在使用yield return返回IEnumerable<T>

我们创建的类MyCollection可以做为复杂集合类的基本实现。但是,如果你不需要实现IEnumerable<T>,那么应可以通过yield return语句实现一个IEnumerable<T>,而不是编写MyCollection这样的类。也就是说你可以把迭代逻辑迁移到一个返回IEnumerable<T>的方法中,然后让编译器来为你完成剩余的事情。

class Program
{
    static void Main(string[] args)
    {

        foreach(int i in GetSomeIntegers())
            Console.WriteLine(i);

        Console.ReadLine();
    }

    static IEnumerable<int> GetSomeIntegers()
    {
        int[] data = { 1, 2, 3 }; 
        foreach (int i in data)
            yield return i;
    }       
}

与之对应的IL代码

image

从IL代码中,我们可以看到,编译器同样生产了一个内部的类,该类实现了IEnumerator<Int32>接口。

3)如果类包含了另外集合,那么需要返回所包含集合的列举器

最后一种实现方式将就是编写一个类直接实现IEnumerator接口。其实这也就是编译器之前做的事情。在实际中,你不需要这么做。

首先我们来实现非Generic的IEnumerator

internal class MyCollection : IEnumerable
{
    int[] data ={ 1, 2, 3 };        

    public IEnumerator GetEnumerator()
    {
        return new Enumerator(this);
    }

    private class Enumerator : IEnumerator
    {
        MyCollection collection;
        int index;

        public Enumerator(MyCollection collection)
        {
            this.collection = collection;
            index = -1;
        }

        public object Current
        {
            get { return collection.data[index]; }
        }

        public bool MoveNext()
        {
            if (index < collection.data.Length-1)
            {
                index++;
                return true;
            }

            return false;
        }

        public void Reset()
        {
            index = -1;
        }
    }
}

然后,我们在上述代码的基础上,实现Generic的IEnumerator

internal class MyCollection : IEnumerable<Int32>
{
    int[] data = { 1, 2, 3 };

    // implement IEnumerable<T>
    public IEnumerator<Int32> GetEnumerator()
    {
        return new Enumerator(this);
    }
    // implement IEnumerable
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    private class Enumerator : IEnumerator<Int32>
    {
        MyCollection collection;
        int index;

        public Enumerator(MyCollection collection)
            {
                this.collection = collection;
                index = -1;
            }

        #region implement IEnumerator<T>
            public int Current
            {
                get { return collection.data[index]; }
            }

            public void Dispose()
            {
            }            

            public bool MoveNext()
            {
                if (index < collection.data.Length - 1)
                {
                    index++;
                    return true;
                }

                return false;
            }

            public void Reset()
            {
                index = -1;
            }
            #endregion

        // implement IEnumerator
        object IEnumerator.Current
        {
            get { return Current; }
        }
    }
}

Generic版本的IEnumerator比非Generic的IEnumberator效率高一些,因为不需要把int转化成object,从而减少了装箱的开销。我们多看一眼此时对应的IL代码:

image

显然地,我们可以看到我们手动创建Enumerator与编译器生成的Enumerator是一样的

 

此外,当我们使用第二种方式的时候,如果我们有多个IEnumerable<T>的方法,那么编译器会产生多个实现了IEnumerator<T>的类

class Program
{
    static void Main(string[] args)
    {

        foreach (int i in GetSomeIntegers())
            Console.WriteLine(i);

        foreach (int i in GetSomeOdds())
            Console.WriteLine(i);

        Console.ReadLine();
    }

    static IEnumerable<Int32> GetSomeIntegers()
    {
        int[] collection = { 1, 2, 3, 4, 5 };
        foreach (int i in collection)
            yield return i;
    }

    static IEnumerable<Int32> GetSomeOdds()
    {
        int[] collection = { 1, 2, 3, 4, 5 };
        foreach (int i in collection)
            if(i%2==1)
                yield return i;
    }       
   
}

对应的IL代码可以看到有两个内部IEnumerator<T>类

image

 

而下面的代码只会产生一个IEnumerator<T>类

class Program
{
    static void Main(string[] args)
    {

        foreach (int i in GetSomeIntegers())
            Console.WriteLine(i);

        foreach (int i in GetSomeOdds())
            Console.WriteLine(i);

        Console.ReadLine();
    }

    static IEnumerable<Int32> GetSomeIntegers()
    {
        return GetDetails();
    }

    static IEnumerable<Int32> GetSomeOdds()
    {
        return GetDetails(true);
    }

    private static IEnumerable<Int32> GetDetails(bool isOdd = false)
    {
        int[] collection = { 1, 2, 3, 4, 5 };
        int index = 0;

        foreach (int i in collection)
        {
            if (isOdd && i % 2 == 1)
                yield return i;
            if (!isOdd)
                yield return collection[index];
            
            index++;
        }
    }   
}

同样地,下面的代码也只会产生一个IEnumerator<T>类

....
static IEnumerable<Int32> GetSomeIntegers()
{
    foreach (int i in GetDetails())
        yield return i;
}

static IEnumerable<Int32> GetSomeOdds()
{
    foreach (int i in GetDetails(true))
        yield return i;
}
....

 

由此,我们可以发现,在实现IEnumerable时,特别是有多个实现时,需要注意尽量减少编译器生成IEnumerator的类的个数。我猜测在内部,编译器应该是根据真正不同的yield return对于的iterator来确定IEnumerator类的个数。在我的示例代码中,产出两个IEnumerator类时,GetSomeIntegers和GetSomeOdds的yield return的iterator是不同的;而在产生一个IEnumerator类时,它们都指向GetDetails的yield return对应的iterator。

 

最后,我们再来看看IEnumeratorIterator

在网上,并没有关于两者的明确区分,或许是我把两个不该混淆的概念混淆了。下面是我自己的看法,如果不正确,欢迎指正:

1) 实现IEnumerator用于实现IEnumerable,与GetEnumerator方法关联在一起,从而可以使用foreach;而且一旦一个类中确定了遍历(MoveNext)的方式之后,那么就只有这一种方式去遍历集合了。.NET Framework中大多数集合的IEnumerator都默认向前只读的方式遍历集合。

2)Iterator用于遍历集合,可以有多个实现方式,唯一的要求是返回IEnumerator<T>,从某种意义上说,Iterator就是IEnumerator。两者的区别是,前者一旦确定,就只能使用这个方式遍历集合然后返回一个IEnumerator;而后者可以在多个方法中以多种方式遍历集合然后返回不同的IEnumerator。(我认为,两者的差别与IComparable和IComparer的差别类似)。

转载于:https://www.cnblogs.com/yang_sy/p/3596457.html

<think> 我们正在处理的是SystemVerilog编译错误,错误代码为SV_NCEL,全称Non-constant enumeration label(非常量枚举标签)。这个错误通常发生在枚举类型定义中,枚举标签(label)必须是一个常量表达式,但实际提供了一个非常量值。 根据SystemVerilog标准,枚举类型中的每个枚举值必须是常量。这意味着在定义枚举类型时,不能使用变量或运行时才能确定的值作为枚举标签的初始值。 例如,以下代码会导致SV_NCEL错误: ```systemverilog module test; int x = 1; typedef enum { RED = x, GREEN, BLUE } color; // 错误:x是非常量 endmodule ``` 这里,枚举标签RED被初始化为变量x的值,而x在编译时不是一个常量(即使它被初始化为1,但按照语言规则,它仍然是一个变量,因为它是运行时赋值的)。因此,编译器报错。 解决方法是确保枚举标签的初始值是一个常量。常量可以是: - 整型常数(如0, 1, 2) - 参数(parameter)或局部参数(localparam) - 其他常量表达式(由常量组成的表达式) 修正上面的例子: ```systemverilog module test; parameter x = 1; // 使用parameter定义常量 typedef enum { RED = x, GREEN, BLUE } color; // 正确:x是常量 endmodule ``` 或者直接使用数字: ```systemverilog module test; typedef enum { RED = 1, GREEN, BLUE } color; // 正确:使用常量 endmodule ``` 所以,解决SV_NCEL错误的关键是检查枚举定义中所有显式赋值的标签,确保其赋值表达式是常量。 另外,如果枚举标签没有显式赋值,则编译器会自动分配一个整型常量(从0开始递增),所以不会出错。 总结: 1. 错误原因:在枚举定义中,为枚举标签赋了一个非常量值。 2. 解决方法:将赋值表达式改为常量(如整型字面值、参数等)。 如果问题复杂,例如常量表达式依赖于其他参数,请确保这些参数也是使用`parameter`或`localparam`定义的。 注意:在SystemVerilog中,枚举基类型可以是整型(int, byte等)或用户自定义的类型,但枚举值必须是常量。 参考:IEEE Std 1800-2012, Section 6.19 Enumerations. 根据上述解释,我们给出以下示例: 错误示例: ```systemverilog module bad_example; logic [1:0] y = 2'b10; typedef enum { A = y, B } my_enum; // 错误:y是非常量 endmodule ``` 正确示例: ```systemverilog module good_example; parameter logic [1:0] y = 2'b10; typedef enum { A = y, B } my_enum; // 正确:y是常量 endmodule ``` 或者: ```systemverilog module good_example; localparam logic [1:0] y = 2'b10; typedef enum { A = y, B } my_enum; // 正确:y是常量 endmodule ``` 如果枚举值需要计算,也必须是常量表达式,例如: ```systemverilog module good_example; parameter int a = 1; parameter int b = 2; typedef enum { A = a+b, B } my_enum; // 正确:a+b是常量表达式 endmodule ``` 最后,请注意,枚举标签的赋值表达式必须是编译时可确定的常量。 希望以上解释能帮助您解决SV_NCEL错误。 §§相关问题§§ 1. SystemVerilog枚举类型中如何定义枚举值范围? 2. SystemVerilog枚举类型有哪些常用方法? 3. 如何在SystemVerilog中遍历枚举类型的所有值? </think>### Error SV_NCEL: Non-constant enumeration label 的含义解决方案 #### **错误含义** - **SV_NCEL** 是 SystemVerilog 编译错误代码,全称为 **Non-constant enumeration label**(非常量枚举标签)。 - 该错误表示在枚举类型(`enum`)定义中,使用了**非常量表达式**作为枚举标签的值。枚举标签必须是编译时可确定的常量(如整数字面量、`parameter` 或 `localparam`),不能是变量或运行时才能计算的值。 - 例如: ```systemverilog int x = 5; typedef enum { RED = x, GREEN } color_t; // 错误:x 是变量,非常量 ``` #### **常见原因** 1. **使用变量赋值枚举标签** ```systemverilog logic [3:0] val = 4; typedef enum { A = val, B } my_enum; // 错误:val 是变量 ``` 2. **使用函数返回值** ```systemverilog function int get_val(); return 10; endfunction typedef enum { A = get_val(), B } my_enum; // 错误:函数调用非编译时常量 ``` 3. **使用动态表达式** ```systemverilog logic [1:0] sel = 2'b01; typedef enum { A = sel + 1, B } my_enum; // 错误:sel 的值在运行时确定 ``` --- #### **解决方法** ##### 1. **使用常量替代变量** - 将变量改为 `parameter` 或 `localparam`(编译时常量): ```systemverilog parameter int x = 5; // 改为参数常量 typedef enum { RED = x, GREEN } color_t; // 正确 ``` ##### 2. **直接使用字面量** - 如果枚举值固定,直接使用数字: ```systemverilog typedef enum { RED = 5, GREEN = 10 } color_t; // 正确 ``` ##### 3. **避免函数调用或动态计算** - 枚举值必须是静态的,不可包含运行时逻辑: ```systemverilog // 错误示例 int y = 2; typedef enum { A = y * 2, B } my_enum; // 错误:y 是变量 // 正确示例 parameter int y = 2; typedef enum { A = y * 2, B } my_enum; // 正确:y 是常量 ``` ##### 4. **使用 `'()` 自动分配值** - 如果不需显式赋值,用 `'()` 让编译器自动分配递增的常量值: ```systemverilog typedef enum { RED, GREEN, BLUE } color_t; // 自动分配:RED=0, GREEN=1, BLUE=2 ``` --- #### **完整示例对比** **错误代码**: ```systemverilog module example; logic [3:0] dyn_val = 4; typedef enum { STATE_A = dyn_val, // 错误:非常量 STATE_B = $random // 错误:随机函数 } state_t; endmodule ``` **修复后代码**: ```systemverilog module example; parameter STATE_INIT = 4; // 声明为常量 typedef enum { STATE_A = STATE_INIT, // 正确:使用参数常量 STATE_B = 8 // 正确:字面量 } state_t; endmodule ``` --- ### 总结 | **问题类型** | **解决策略** | |--------------------|----------------------------------| | 变量赋值枚举标签 | 改用 `parameter` 或 `localparam` | | 函数调用或动态计算 | 替换为编译时可确定的常量表达式 | | 需显式指定枚举值 | 使用整数字面量或常量参数 | | 无需显式赋值 | 用 `'()` 自动分配值 | 通过确保枚举标签的值在编译时恒定,即可解决 SV_NCEL 错误[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值