专注收集记录技术开发学习笔记、技术难点、解决方案
网站信息搜索 >> 请输入关键词:
您当前的位置: 首页 > WinForm

会合与泛型

发布时间:2011-06-23 13:53:45 文章来源:www.iduyao.cn 采编人员:星星草
集合与泛型

集合类负责存储一系列的个体,其集合的长度可能是不变的或者可变的。相比于普通的数组结构,集合类的功能更加具体。

集合类分为普通(非泛型)集合和泛型集合。泛型集合类的命名空间为集合的一个子命名空间:System.Collections.Generic。非泛型的集合中,所有的成员都被当作为object类型,不同的成员可以拥有不同的数据类型。而泛型的集合所有的成员类型都是相同的。

非泛型集合

所有非泛型集合都实现接口ICollection。

所有非泛型集合中的成员都是object。

集合类的基础接口ICollection

所有非泛型集合都实现这个接口。这个接口继承了IEnumerable接口,所以可以进行遍历。另外,该接口提供以下主要属性功能:

  • Count:获取这个集合中包含的成员数
  • AsParallel:允许并行查询
  • AsQueryable:允许将该类转化为IQueryable

ICollection接口扩展 IEnumerable;IDictionary 和 IList 则是扩展 ICollection 的更为专用的接口。 IDictionary 实现键/值对的集合,如 Hashtable 类。不能通过索引的方式访问实现了IDictionary的对象,只能提供一个键,然后查找是否存在值为该键的成员。IList 实现值的集合,其成员可通过索引访问,如 ArrayList 类。

IList接口(非泛型版本)与ArrayList

相比抽象的基础接口ICollection,IList更加具体,其提供了add(插入末尾), insert(插入指定项之后), remove, removeat, clear等基本的插入,删除等操作。实现IList有三种方法,可以只读(无法修改),声明大小和不声明大小,一般都不声明大小。

ArrayList是一个实现了IList接口的类型,相比Array,其插入删除更加灵活。Array的插入非常麻烦,因为如果其后还有数据,则还要手动将所有后面的数据推移一位。而ArrayList(或者说所有实现了IList接口的类型)都不需要考虑这个问题。这是它和数组的第一个区别。第二个区别则是因为它是非泛型集合,所以其所有成员都被看做Object。第三个区别就是ArrayList的容量是可变的,所以永远不会溢出。当新声明一个ArrayList时,如果没有同时插入任何成员,则其容量为零。当插入成员时,系统自动调整ArrayList的容量,调整的幅度是从4开始倍增,即如果一次插入5个成员,则系统将ArrayList的容量调整为8,如果只插入1个成员,则容量为4。因为通常时候,我们都是不知道数组长度的,所以使用ArrayList,可以方便的存储数据还不用担心溢出问题。当ArrayList的成员小于容量时,虽然空间有所浪费,但其浪费的空间数相比数组来说通常会经济些。

IList接口:其他非泛型集合

其他非泛型集合包括了队列,栈等。他们顾名思义,实现的是队列和栈的数据结构。这些数据结构通常没有太大用处。

IDictionary接口与Hashtable

哈希表可以看作是字典的非泛型版本。其存储若干键值对,通过键来访问。由于有了泛型,查找非泛型的哈希表的效率远远低于泛型的字典。所以现在哈希表的使用场合较少。

SortedList

SortedList比较特殊,和Hashtable 两者都是表示键/值对的集合,但hashtable是没有排序的,所以新增元素会比较快。而SortedList 存储的键值对是按key进行排序了的,因为要排序,所以新增元素时,要先查找元素的位置再插入,相对慢些,但是在查找时比较快。                

线程安全的集合类System.Collection.Concurrent【.NET 4.0+】

System.Collections.Concurrent命名空间提供多个线程安全集合类。当有多个线程并发访问集合时,应使用这些类代替 System.Collections 和 System.Collections.Generic 命名空间中的对应类型。我们就用ConcurrentDictionary作为例子。当多个线程访问ConcurrentDictionary时,线程安全保证了成员不会被添加或者删除多次。

当我们添加一个键值对<a,b>到ConcurrentDictionary中,我们不再使用Add,而是使用方法AddOrUpdate。当键a不存在时,插入并返回对应的值b,如果键a存在,则更新该键所对应的值为b。相比于Dictionary和hashtable类型,所有的操作都是原子的,因而也是线程安全的。

泛型集合

泛型集合都是类型安全的。

泛型集合属于命名空间System.Collections.Generic。

优点

c#2.0引入了泛型。其主要解决非泛型集合的两个缺点,一是非泛型集合的插入和删除时的装箱和拆箱,二是防止非泛型集合类型不安全的问题,这些问题在编译时都是查不出来的,而只有在运行时才会知道。例如你有一个非泛型的ArrayList,你期望其中都是整数,但当你取出来进行加减运算时,如果有一个是字符串,则就会出现异常。

泛型的声明需要一个占位符<T>,此处T可以是某种类型的名称或其他任意字符串。如果是某种类型的名称,例如List<string>此时该List就是类型安全的,List里面的所有对象都被看做字符串。如果仅仅是某个非关键字的字符串例如Tkey,则这个类型可能范围比较大,例如包括所有的值类型等。你需要用某种方法令编译器知道你的类型究竟是继承自哪里。可以使用类型约束帮助编译器进行推断。

和非泛型集合的接口结构类似,几乎所有的非泛型集合都有对应的泛型版本。ICollection<T>继承了IEnumerable<T>,然后IList<T>和IDictionary<T>又继承了他。List<T>和Dictionary<TKey, TValue>分别继承了前面的IList<T>和IDictionary<T>。其继承路线是这样的:

IEnumerable<KeyValuePair(TKey,TValue)> - ICollection<KeyValuePair(TKey,TValue)> - IDictionary< TKey, TValue> - Dictionary<TKey, TValue>

IEnumerable<T> - ICollection<T> - IList<T> - List<T>

 

泛型方法和类型约束

泛型方法就是方法的输入或者输出带有泛型的方法。当你有很多完全一样(仅仅是输入输出的类型不同)的方法时,你可以考虑写一个泛型方法将其统一起来。例如你想写一个方法比较任意两个同类型对象的大小:

 

        public static T GetBiggerOne<T>(T itemOne,   T itemTwo)

        {

            if (itemOne.CompareTo(itemTwo) >   0)

            {

                return itemOne;

            }

            return itemTwo;

        }

 

但这段代码是有问题的。因为CompareTo不是对所有类型都有定义。它只对实现了接口IComparable的类型才有意义。所以我们还要限制T必须实现了IComparable,我们可以使用类型约束。

 

类型约束的格式是where T :类型名称或其他约束,有如下几种:

1 引用/值类型约束:当后跟where T : class时,T必须是引用类型。当后跟where T : struct时,T必须是值类型。这个约束必须是第一个约束(约束都是从大到小)。注意引用和值类型约束不可能同时成立。

2 其他类型约束:后跟类型名称,可以跟多个。只能指定一个类(c#不支持多重继承所以不可能有类型继承两个类,所以指定多于一个类是永远无法满足约束的),但可以指定多个接口(唯一实现多重继承的方法就是使用接口)。

3 构造函数约束:where T : new(),此时T必须有一个无参数的构造函数。注意这个约束必须是最后一个。注意不能写一个拥有有参数构造函数的约束,例如where T : new(string) 是不合法的。

 

所以要让代码运行,必须加上这样的约束:

 

        public static T GetBiggerOne<T>(T itemOne,   T itemTwo) where T : IComparable

        {

            if (itemOne.CompareTo(itemTwo) >   0)

            {

                return itemOne;

            }

            return itemTwo;

        }

 

类型推断

当有某个泛型方法时,外部调用该泛型方法将可以不用指定类型,编译器会帮我们推断类型。例如调用上面的方法,以下两种都是可以运行的。

 

            GetBiggerOne(1, 2);

            GetBiggerOne<string>("a", "b");

 

泛型委托

自从有了泛型委托之后,定义委托就可以不需要delegate了,换句话说,func和action接管了全部的工作。通常来说,泛型委托和匿名方法或者lambda表达式一起使用。这将会大大简化委托的代码书写量。

Action: 输入可以有0-16个,不能有输出

Func: 输入可以有0-16个,有一个输出

Predicate:输入是一组条件,有一个布尔类型的输出,用的不多,通常作为其他方法的输入,例如list<T>.FindAll

泛型的协变和逆变

协变和逆变统称可变性,可变性的定义是以某种安全的方式,将一个类型转换为另一个类型来使用。通常,这两个类型存在继承关系。协变性:转换的方向是从派生类型到基类型。逆变正好相反。不具有协变和逆变的特性的对象则称之为具有不变性(invariable),注意这个和字符串的不变性不同,其英文是immutable。

 

协变和逆变的用途

考虑如下的一个简单接口,其对任意的类型返回一个实例:

 

    interface GetInstance<T>

    {

        T Create();

    }

 

    public class Aclass : GetInstance<string>

    {

        //字符串工厂

        public string Create()

        {

            return "";          

        }

    }

 

此时,T只是一个返回值,这意味着我们可以将特定类型的工厂视为一般的工厂。此时,我们用到了协变性,其将较小(范围)的值转变为较大的值。

 

逆变性则正好相反,这意味着T是一个传入的值,接口会消费它而不是产生它。下面的接口消费了T, 因为T作为参数出现在了接口的签名中。所以,如果我们实现了Print<object>,理论上我们就可以打印任何东西。此时,我们用到了逆变,其将较大的值转变为较小的值。

 

    interface Print<T>

    {

        void Print(T input);

    }

 

    public class Bclass : Print<object>

    {

        public void Print(object o)

        {

            Console.WriteLine(o.ToString());

        }

    }

 

当上面两件事同时发生时,这个接口将自动失去协变和逆变的特性,变成一个不变体。在c#4.0中,我们通过out和in关键字实现协变和逆变性。这个特性只能用在接口和委托上。

当声明为“out”时,代表它是用来返回的,只能作为结果返回,中途不能更改。

当声明为"in"时,代表它是用来输入的,只能作为参数输入,不能被返回。

IEnumerable<T>的协变性(out)

在语法上,不可以隐式的将list<string>转换为list<object>。

 

List<string> aa = new List<object>();

 

这是因为虽然string继承object,但list<string>和list<object>没有继承关系。同理,调转方向也是错的。

 

List<object> bb = new List<string>();

 

但这样做却是对的:

 

IEnumerable<string> aa = new List<string>();

IEnumerable<object> bb = aa;

 

这是因为IEnumerable<T>具有协变性,其签名为:

 

public interface IEnumerable<out T> : IEnumerable

 

看到out关键字了么,这个关键字赋予了IEnumerable<T>协变的特性。

IComparer<T>的逆变性(in)

实现了接口IComparer<T>的类可以比较大小,这是常识了。显然,如果T是父类,则可以传入其任意的子类。这就是逆变性。接口签名为:

 

public interface IComparer<in T>

 

类似的.net接口和委托还有:

 

接口:          

  •             IQueryable<out T>
  •             IEnumerator<out T>
  •             IGrouping<out TKey,out TElement>
  •             IEqualityComparer<in T>
  •             IComparable<in T>

委托:

  •             System.Action<in T>
  •             System.Func<Out Tresult>
  •             Predicate<in T>
  •             Comparison<in T>
  •             Converter<in TInput,out TOutput>

 

应用

我们自己定义泛型接口的时候也可以使用协变和逆变,我们不妨来看一个示例,来体现协变的特征。

 

interface 接口<out T>
{
   T 属性 { get; set; }
}

 

我定义一个接口,一个具有get和set访问器的属性,然而,编译是报错的,提示:变体无效: 类型参数“T”必须为对于“test.接口<T>.属性”有效的 固定式。“T”为协变。正因为我声明了T为协变,所以,T只能被返回,不允许被修改,所以,如果去掉“set”访问器,才可以编译通过。同样,如果我在“接口”中声明一个方法: void 方法(T t) 同样是会报错的,T被声明了协变,“方法(T t)”就不能存在。

 

逆变的例子:

interface 接口<in T>
{
   void 方法(T t);
}
class 类<T> : 接口<T>
{
   public void 方法(T t){}
}

 

声明“in”不允许被返回,但是可以进行更改。

static void Main(string[] args)
{
     接口<车子> 一群车子 = new 类<车子>();
     接口<汽车> 一群汽车 = 一群车子;
}

 

因为“接口”声明了“in”关键字,声明为逆变,所以现在可以将父类车子转化为子类汽车。让参数去接受一个相对更“弱“的类型,其实是让一个参数的类型,更加具体化,更明确化的一个过程。

友情提示:
信息收集于互联网,如果您发现错误或造成侵权,请及时通知本站更正或删除,具体联系方式见页面底部联系我们,谢谢。

其他相似内容:

热门推荐: