集合、拆箱、装箱、自定义集合的foreach

时间:2023-03-08 15:33:44

集合部分

  参考:http://msdn.microsoft.com/zh-cn/library/0ytkdh4s(v=vs.110).aspx

  集合类型是诸如哈希表、队列、堆栈、包、字典和列表等数据集合的常见变体。集合基于 ICollection 接口、IList 接口、IDictionary 接口,或其泛型集合中的相应接口。每一个元素只包含一个值。

  IList 接口和 IDictionary 接口都是从 ICollection 接口派生的;因此,所有集合都直接或间接基于 ICollection 接口。

  基于 IList 接口的集合,如 Array、ArrayList 或 List<T>。

  基于 ICollection 接口的集合,如Queue、ConcurrentQueue<T>、 Stack、 ConcurrentStack<T> 或 LinkedList<T>。

  基于 IDictionary 接口的集合,如 Hashtable 和 SortedList 类、Dictionary<TKey, TValue> 和 SortedList<TKey, TValue> 泛型类。

  基于 ConcurrentDictionary<TKey, TValue> 类的集合中,每个元素都包含一个键和一个值。

  KeyedCollection<TKey, TItem> 类较为特别,因为它是值中带有嵌入键的值列表,因此它的行为既像列表又像字典。

  1)数组集合类型

  Array 类不是 System.Collections 命名空间的一部分。 但是,该类仍是一个集合,因为它基于 IList 接口。

  Array 对象的秩(注:下标)是 Array 中的维数。 一个 Array 可以具有一个或多个秩。

  Array 的下限是其第一个元素的索引。 一个 Array 可以具有任何下限。 默认情况下它的下限为零,但在使用 CreateInstance 创建 Array 类的实例时可以定义不同的下限。

  与 System.Collections 命名空间中的类不同,Array 具有固定的容量。 若要增加容量,必须创建具有所需容量的新 Array 对象,将旧 Array 对象中的元素复制到新对象中,然后删除该旧 Array。但是,只有系统和编译器可以从 Array 类显式派生。 用户应使用其使用的语言所提供的数组构造。

  2)ArrayList和List集合类型

  ArrayList 类和 List<T> 泛型类提供多数 System.Collections 类都提供但 Array 类未提供的一些功能。 例如:

    • Array 的容量是固定的,而 ArrayList 或 List<T> 的容量可根据需要自动扩充。 如果更改了 ArrayList.Capacity 属性的值,则可以自动进行内存重新分配和元素复制。
    • ArrayList 和 List<T> 提供添加、插入或移除某一范围元素的方法。 在 Array 中,您只能一次获取或设置一个元素的值。
    • 通过使用 Synchronized 方法可以很容易地创建 ArrayList 的同步版本;但是,此同步类型的效率相对较低。Array 和 List<T> 类将实现同步的任务留给了用户。 System.Collections.Concurrent 命名空间不提供并发列表类型,但它提供 ConcurrentQueue<T> 和 ConcurrentStack<T> 类型。
    • ArrayList 和 List<T> 提供将只读和固定大小包装返回到集合的方法; 而 Array 则不会提供。

  另一方面,Array 提供了 ArrayList 和 List<T> 所缺少的某些灵活性。 例如:

    • 可以设置 Array 的下限,但 ArrayList 或 List<T> 的下限始终为零。
    • Array 可以具有多个维度,而 ArrayList 或 List<T> 始终只具有一个维度。 但是可以轻松创建数组列表或列表的列表。
    • 特定类型(Object 除外)的 Array 的性能优于 ArrayList 的性能。 这是因为 ArrayList 的元素属于 Object 类型;所以在存储或检索值类型时通常发生装箱和取消装箱操作。 不过,在不需要重新分配时(即最初的容量十分接近列表的最大容量),List<T> 的性能与同类型的数组十分相近。
    • 需要数组的大多数情况都可以改为使用 ArrayList 或 List<T>;它们更容易使用,并且一般与相同类型的数组具有相近的性能。

  Array 位于 System 命名空间中;ArrayList 位于 System.Collections 命名空间中;List<T> 位于 System.Collections.Generic 命名空间中。  

  3) Hashtable和Dictionary集合类型

  Hashtable 对象由包含集合元素的存储桶组成。 存储桶是 Hashtable 中各元素的虚拟子组,与大多数集合中进行的搜索和检索相比,存储桶可令搜索和检索更为便捷。 每个存储桶都与一个哈希代码关联,该哈希代码是使用哈希函数生成的并且基于相应元素的键。泛型 HashSet<T> 类是一个用于包含独特元素的无序集合。

  哈希函数是基于键返回数值哈希代码的算法。 键是正被存储的对象的某一属性的值。 哈希函数必须始终为相同的键返回相同的哈希代码。 一个哈希函数能够为两个不同的键生成相同的哈希代码,但从哈希表检索元素时,为每一唯一键生成唯一哈希代码的哈希函数将令性能更佳。

  在 Hashtable 中用作元素的每一对象必须能够通过使用 GetHashCode 方法的实现为其自身生成哈希代码。 但是,还可以通过使用接受 IHashCodeProvider 实现作为参数之一的 Hashtable 构造函数,为 Hashtable 中的所有元素指定一个哈希函数。

  在将一个对象添加到 Hashtable 时,它被存储在存储桶中,该存储桶与匹配该对象的哈希代码的哈希代码关联。 在 Hashtable 内搜索一个值时,将为该值生成哈希代码,并且搜索与该哈希代码关联的存储桶。

  Dictionary<TKey, TValue> 和 ConcurrentDictionary<TKey, TValue>类与 Hashtable 类的功能相同。 特定类型(Object 除外)的 Dictionary<TKey, TValue> 提供了比值类型的 Hashtable 更好的性能。 这是因为 Hashtable 的元素属于 Object 类型;所以在存储或检索值类型时通常发生装箱和取消装箱操作。 当有多个线程可能同时访问该集合时,应使用 ConcurrentDictionary<TKey, TValue>类。

  别的集合也不再进行介绍了,感兴趣的话可以自己去查看msdn。

小结

  ArrayList可变长数组,使用类似于数组(其实内部也是用的数组,只是增加到一定长度时,会把数据克隆到一个新数组中,同时删除旧数组)。要注意的是,向数组中添加数据时,数组的长度不够时会自动扩增,这时删除增加的数据,但数组的长度维持不变,可用arrList.TrimToSize()收缩总容量。当然也可以手动增加长度,操作的属性是Capacity([N]的倍数翻倍增加)。集合常用操作是增删改查遍历,常用的方法是add()、addRange(lcollection c)、 Remove() RemoveAt()、 Clear()、Comtains()、 ToArray()、 Sort()排序、 Reverse()反转、Count统计个数。因为要经过装箱和拆箱,所以此集合并不建议使用,常用的是泛型List<>。

  HashTable键值对集合,使用时有点类似于使用字典,目的是为了方便快速的查找。只要给HashTable一个键,HashTable就可以根据这个键去算出存放信息的地址。

  常用的方法:

    添加Add(object key,object value)、判断hash["key"]、赋值has["key"]="修改";判断是否存在ContainsKey("key");移除Remove("key");

  常用的遍历方法:

遍历键值:

foreach(DictionaryEntry itm in Table){Console.WriteLine(item.Key+item.Value)}

遍历键:

foreach(var item in table.keys){Console.WriteLine(item);}

  Dictionary和HashTable的功能类似,只是Dictionary<TKey, TValue> 提供了比值类型的 Hashtable 更好的性能。

  通常用法:

Dictionary<,> dict=new Dictionary<,>();

//遍历键

foreach(* in dict.key){}

//遍历值

foreach(string item in dict.Values){}

//键值同时遍历

     foreach(KeyValuePair<,> kv int dict){"键{0},值{1}",kv.key,kv.Value}

Foreach实现遍历

 自定义的集合一般是不能被Foreach循环遍历的,要想自定义集合能被遍历类中需要具有GetEnumerator()方法。

 在 C# 中,集合类不必通过实现 IEnumerable 和 IEnumerator 来与 foreach 兼容。 如果此类具有所需的 GetEnumerator、MoveNext、Reset 和 Current 成员,则可与 foreach 结合使用。

  一般的类实现IEnumerable标志可被遍历,实现IEnumerator接口来实现遍历。当然尽量是使用接口,这样有助于规范化。

   实现 IEnumerator 和 IEnumerable,代码示例使用了数组的枚举方法(GetEnumerator、MoveNext、Reset 和 Current)。

  自定义集合中实现Foreach的代码:使用了接口

 using System.Collections;

 // Declare the Tokens class. The class implements the IEnumerable interface.
 public class Tokens : IEnumerable
 {
     private string[] elements;

     Tokens(string source, char[] delimiters)
     {
         // The constructor parses the string argument into tokens.
         elements = source.Split(delimiters);
     }

     // The IEnumerable interface requires implementation of method GetEnumerator.
     public IEnumerator GetEnumerator()
     {
         return new TokenEnumerator(this);
     }

     // Declare an inner class that implements the IEnumerator interface.
     private class TokenEnumerator : IEnumerator
     {
         ;
         private Tokens t;

         public TokenEnumerator(Tokens t)
         {
             this.t = t;
         }

         // The IEnumerator interface requires a MoveNext method.
         public bool MoveNext()
         {
             )
             {
                 position++;
                 return true;
             }
             else
             {
                 return false;
             }
         }

         // The IEnumerator interface requires a Reset method.
         public void Reset()
         {
             position = -;
         }

         // The IEnumerator interface requires a Current method.
         public object Current
         {
             get
             {
                 return t.elements[position];
             }
         }
     }

     // Test the Tokens class.
     static void Main()
     {
         // Create a Tokens instance.
         Tokens f = new Tokens("This is a sample sentence.", new char[] {' ','-'});

         // Display the tokens.
         foreach (string item in f)
         {
             System.Console.WriteLine(item);
         }
     }
 }
 /* Output:
     This
     is
     a
     sample
     sentence.
 */

  未使用接口的代码

using System.Collections;

// Declare the Tokens class. The class implements the IEnumerable interface.
public class Tokens
{
    private string[] elements;

    Tokens(string source, char[] delimiters)
    {
        // The constructor parses the string argument into tokens.
        elements = source.Split(delimiters);
    }

    // The IEnumerable interface requires implementation of method GetEnumerator.
    public TokenEnumerator GetEnumerator()
    {
        return new TokenEnumerator(this);
    }

    // Declare an inner class that implements the IEnumerator interface.
    public class TokenEnumerator
    {
        ;
        private Tokens t;

        public TokenEnumerator(Tokens t)
        {
            this.t = t;
        }

        // The IEnumerator interface requires a MoveNext method.
        public bool MoveNext()
        {
            )
            {
                position++;
                return true;
            }
            else
            {
                return false;
            }
        }

        // The IEnumerator interface requires a Reset method.
        public void Reset()
        {
            position = -;
        }

        // The IEnumerator interface requires a Current method.
        public object Current
        {
            get
            {
                return t.elements[position];
            }
        }
    }

    // Test the Tokens class.
    static void Main()
    {
        // Create a Tokens instance.
        Tokens f = new Tokens("This is a sample sentence.", new char[] { ' ', '-' });

        // Display the tokens.
        foreach (string item in f)
        {
            System.Console.WriteLine(item);
        }
        System.Console.Read();
    }
}
/* Output:
    This
    is
    a
    sample
    sentence.
*/

  省略接口也有一个好处:即,可以比 Object 更为具体地定义 Current 的返回类型。 这会提供类型安全。

  省略 IEnumerable 和 IEnumerator 的缺点是:集合类不再与其他公共语言运行时语言的 foreach 语句或等效语句交互。  

  小结:任何类型,只要想使用foreach来循环遍历,就必须在当前类型中存在:public IEnumerator GetEnumerator()方法。(一般情况我们会通过实现IEnumerable接口来创建该方法)。这个方法的作用并不是用来遍历的,而是用来获取一个对象,这个对象才是用来遍历的。就像想吃西瓜,找卖西瓜的人,而不是去找种西瓜的。

foreach(string item in p){Console.WriteLine(item)};
等价于
IEnumerator etor=p.GetEnumertor();
while(etor.MoveNext()){
string str=etor.Current.ToString();
Console.WriteLine(str);}

    参考:http://msdn.microsoft.com/zh-cn/library/ybcx56wz.aspx  集合

       http://msdn.microsoft.com/zh-cn/library/9yb8xew9.aspx  Foreach

      Dictionary中有一个存储键值对的区域,这个区域的每个存储单元有地址编好,根据hashCode算法,计算key的值的键值对应该应该存储的地址,将键值对放入指定的地址即可。查找的时候首    先计算key的地址,就可以找到数据了。根据key查找房间号,而不是逐个房间查找。
    为什么Dictionary那么快?

    当把一个kvp,采用一耳光固定算法(散列算法)根据key来计算这个key存放的地址。取的时候也是根据要找的key可以快速算出kvp存放的地址。

装箱和拆箱

  装箱:将 值类型 转换为 引用类型 的过程叫,装箱。

  拆箱:将 引用类型 转换为 值类型 的过程叫,拆箱。

  装箱、拆箱就是:值类型→引用类型 或引用类型→值类型。

  “箱”指的就是托管堆,装箱即指在托管堆中将在栈上的值类型对象封装,生成一份该值类型对象的副本,并返回该副本的地址。而拆箱即是指返回已装箱值类型在托管堆中的地址(注意:严格意义来说拆箱是不包括值类型字段的拷贝的)。

  装箱的过程为:

    1. 分配内存: 在托管堆中分配好内存,内存的大小是值类型的各个字段需要的内存量加上托管堆的所有对象都有的两个额外成员—类型对象指针和同步块索引—所需要的内存量之和。
    2. 复制对象: 将值类型的字段复制到新分配的内存中。
    3. 返回地址: 将已装箱的值类型对象的地址返回给引用类型的变量。

  简单的说下,装箱的时候,使用什么类型来装箱,拆箱的时候必须使用对应的类型来拆箱,不然会抛异常。装箱和拆箱还和继承有关系,值类型可以装箱到父类的引用类型,但是不能装箱到和他无关的引用类型。

  例如:我们常用的Int32,转到Int32类型定义进行查看会发现:

  struct Int32 : IComparable, IFormattable, IConvertible, IComparable<int>, IEquatable<int>

  继承的父类是干嘛的,我们这不做讨论,我们再来看一段代码:

int n1=10;

IComparable com=n1;

int m=(int)com;

Console.WriteLine(m);

  由于接口是引用类型,int32是结构,所以Int32到IComaparable发生了装箱,IComaparable到Int32发生了拆箱。

  即:IComparable com=n1//发生了装箱

    int m=(int)com;//拆箱

  msdn上的定义:装箱是将值类型转换为 object 类型或由此值类型实现的任何接口类型的过程。 当 CLR 对值类型进行装箱时,会将该值包装到 System.Object 内部,再将后者存储在托管堆上。 取消装箱将从对象中提取值类型。 装箱是隐式的;取消装箱是显式的。 装箱和取消装箱的概念是类型系统 C# 统一视图的基础,其中任一类型的值都被视为一个对象。

  以前学装箱和拆箱时,只知道值类型转换为object为装箱,object到int类型为拆箱。现在回头才发现原来不仅仅如此,不过值类型就那么几种,他的父类我们并不常用,转换时通常就是用object类。

  有些人觉得int和string类型可以进行转换,觉得符合装箱的定义,但是要知道string类型不是int的父类,他们之间只是发生了转换,并没有发生装箱。还有一点,拆箱出来的时候还得用原来的类型。 

还有一些装箱和拆箱藏的是比较深的,例如: 

Int  i=20;

i.getType();

  查看反编译会发现此处进行了装箱,那为什么会发生装箱呢?

  值类型调用父类的方法。若调用的是基类的非虚方法,无论如何都会装箱;若调用的是虚方法,如果在值类型中重写了,那么就不会装箱,若没有重写,调用的仍然是基类的方法,那么这个值类型仍然会装箱。

  注意:

1. 装箱和拆箱都是针对值类型而言的,而引用类型一致都是在托管堆中的,即总是以”装箱“的形式存在。

2. 装箱和拆箱并不是互逆的过程,实际上装箱的性能开销远比拆箱的性能开销大,并且伴随着拆箱的字段复制步骤实际上不属于拆箱。

3. 只有是值类型装箱之后的引用类型才能被拆箱,而并不是所有的引用类型都能被拆箱,将非装箱实例强制转化为值类型或者转化为非原装箱的值类型,会抛出InvalidCastException异常。

4. 拆箱的IL代码中有unbox和unbox.any两条指令,他们的区别是unbox指令不包含伴随着拆箱的字段复制操作,但是unbox.any则包含 伴随着拆箱的字段复制操作。我到目前为止没有发现C#中有没有字段复制操作的拆箱,所以有时候也把这部操作放在拆箱的步骤里。

5. 在我们拆箱前怎么知道这个引用类型是否是期望的那个值类型的装箱形式呢。我们有两种方法,一种是用is/as操作符来判断,还有一种方法是object类的GetType方法。

拆箱的过程

  取消装箱是从引用类型到值类型或从接口类型到实现该接口的值类型的显式转换。两步走:

  1. 检查实例:首先检查变量的值是否为null,如果是则抛出NullReferenceException异常;再检查变量的引用指向的对象是不是给定值类型的已装箱对象,如果不是,则抛出InvalidCastException异常。
  2. 返回地址:返回已装箱实例中属于原值类型字段的地址,而两个额外成员(类型对象指针和同步块索引)则不会返回。

  注意:拆箱是,必须用装箱时的类型来拆箱。

如何避免装箱

  装箱和拆箱会造成相当大的性能损耗(相比之下,装箱要比拆箱性能损耗大),性能问题主要体现在执行速度和字段复制上。因此我们在编写代码时要尽量避免装箱和拆箱,常用的手段为:

  1. 使用重载方法。为了避免装箱,很多FCL中的方法都提供了很多重载的方法。比如我们之前讨论过的Console.WriteLine方法,提供了多达19个重载方法,目的就是为了减少值类型装箱的次数。
  2. 使用泛型。因为装箱和拆箱的性能问题,所以在.NET 2.0中引用了泛型,他的主要目的就是避免值类型和引用类型之间的装箱和拆箱。
  3. 如果在项目中一个值类型变量需要多次拆装箱,那么可以将这个变量提出来在前面显式装箱。比如下面这段代码:

int j = 3;

ArrayList a = new ArrayList();

for (int i = 0; i < 100; i++)

{  a.Add(j);}

    可修改为:

     int j = 3;

object ob = j;

     ArrayList a = new ArrayList();

     for (int i = 0; i < 100; i++){  a.Add(ob);}

  ToString。这点单独列出来是因为虽然小,但是很实用。虽然表面上看值类型调用ToString方法是要进行装箱的,因为ToString是从基类继承的方法。但是ToString方法是一个虚方法,值类型一般都重写了这个方法,所以调用ToString方法不会装箱。

注意:String.Format方法容易造成装箱,避免的最佳方法就是在调用这个方法前将所有的值类型参数都调用一次ToString方法。

小结:

  装箱、拆箱必须是值类型和引用类型之间的转换,且值类型和这个引用类型得是继承关系。这样就可以分清什么时候发生了拆装箱,什么时候发生了转换。

  注意方法传参时可能会发生装箱的,例如传入的参数可以是object类型,传入了值类型,此时即发生了装箱。引用类型和引用类型直接不会发生装箱。传参时传入的引用类型顶多发生了转换。传入的引用类型实际上是一个新的对象,这个对象指向传入参数的地址。

  值类型是可以实现接口的。

  可以参考:http://msdn.microsoft.com/zh-cn/library/yz2be5wk.aspx

自问自答

为什么要用集合而不用数组?

数组类型固定,长度固定,而集合是自动增加的。