C# 泛型集合
考虑到泛型在集合类型中的广泛应用,这里一起讨论。
1. 泛型
1.1 泛型的定义与约束
创建泛型方法、委托、接口或类时,需要在名称后增加尖括号及其中的泛型参数,泛型参数通常用T或T为前缀的描述性单词表示。
/// <summary> /// 集合类型扩展方法 /// </summary> public static class CollectionExt { /// <summary> /// 集合类型判空 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="collection"></param> /// <returns></returns> public static bool IsNullOrEmpty<T>(this ICollection<T> collection) { return collection == null || collection.Count == 0; } }
可用where来约束泛型继承的基类或实现的接口,并且可以使用default(T)获取泛型的默认值。
/// <summary> /// 获取集合中按照某个数值类型比较 最小的元素 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="items"></param> /// <param name="func"></param> /// <returns></returns> public static T MinItem<T>(this IEnumerable<T> items, Func<T, decimal> func) where T : class { List<decimal> list = new List<decimal>(); if (items.IsNullOrEmpty()) { return null; } decimal minValue = decimal.MaxValue; T minItem = default(T); foreach (var item in items) { var currentValue = func(item); if (minValue > currentValue) { minItem = item; minValue = currentValue; } } return minItem; }
1.2 泛型中的类型膨胀
面试的时候,常常会被问到C#和JAVA中泛型的差异,以及泛型参数类型在何时确定。在C#中,仍然以第一段代码中集合类型判空为例分析。
class Program { static void Main(string[] args) { var values = new List<int>(); var strs = new List<string>(); Console.WriteLine(values.IsNullOrEmpty()); Console.WriteLine(strs.IsNullOrEmpty()); Console.ReadKey(); } }
查看IL代码。
.entrypoint // 代码大小 44 (0x2c) .maxstack 1 .locals init ([0] class [mscorlib]System.Collections.Generic.List`1<int32> values, [1] class [mscorlib]System.Collections.Generic.List`1<string> strs) IL_0000: nop IL_0001: newobj instance void class [mscorlib]System.Collections.Generic.List`1<int32>::.ctor() IL_0006: stloc.0 IL_0007: newobj instance void class [mscorlib]System.Collections.Generic.List`1<string>::.ctor() IL_000c: stloc.1 IL_000d: ldloc.0 IL_000e: call bool ConsoleApplication1.CollectionExt::IsNullOrEmpty<int32>(class [mscorlib]System.Collections.Generic.ICollection`1<!!0>) IL_0013: call void [mscorlib]System.Console::WriteLine(bool) IL_0018: nop IL_0019: ldloc.1 IL_001a: call bool ConsoleApplication1.CollectionExt::IsNullOrEmpty<string>(class [mscorlib]System.Collections.Generic.ICollection`1<!!0>) IL_001f: call void [mscorlib]System.Console::WriteLine(bool) IL_0024: nop IL_0025: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey() IL_002a: pop IL_002b: ret
可以看到,泛型的参数类型在编译时就已经确定。
编译后生成的List`1<int32>和List`1<string>,是两个不同的类型,这称为类型膨胀,基于这种方法实现的泛型被称为真实泛型。
这就使得类型安全,避免了装箱和拆箱操作。
2. 集合类型
System.Array定义了数组类。 System.Collections命名空间中定义了处理object对象的常用集合类,在访问时需类型转换或装箱拆箱。 System.Collections.Generic命名空间中提供了很多集合类的泛型版本。 System.Collections.Specialized命名空间中定义了专用于特定类型的集合类。 .NET 4.0开始提供System.Collections.Concurrent命名空间中的多个线程安全的集合类。
不急,慢慢来。
之前遇到几个工作七八年的竟然也不懂这些很基础的东西。
可以说劣币驱逐良币,也是离开目前公司的主要原因,不过社会本身如此。不管怎样,还是多看看、多学习,进而跳出牢笼,重新认识自我和社会,才有可能抓住转瞬即逝的机会。
从自身的角度,觉得技术和成长好厉害、解决很多问题,其实是没有用滴,老板考虑的始终是用较为低廉的薪水招到能做一般事情的人就行了,毕竟大家基本都是业务驱动的公司,能卖钱才是正道理,所以能说的永远比能做事情的机会多。
2.1 IEnumerable
简单的说,实现此接口,才能支持遍历,接口返回的是一个IEnumerator。
[ComVisible(true)] [Guid("496B0ABE-CDEE-11d3-88E8-00902754C43A")] public interface IEnumerable { [DispId(-4)] IEnumerator GetEnumerator(); }
2.2 IEnumerator
真正的实现了遍历的过程,还延伸到yield的用法,这里不再赘述。
[ComVisible(true)] [Guid("496B0ABF-CDEE-11d3-88E8-00902754C43A")] public interface IEnumerator { object Current { get; } bool MoveNext(); void Reset(); }
Current当前枚举项的引用,初始化时尚未指向任何枚举项,每次访问时要转化为对应类型,故多次使用当前枚举项时可先赋值给强类型的临时变量,但泛型版本则不需要这样做。
MoveNext()移动引用使其指向下一个枚举项,首次移动后指向第一个枚举项,重复整个过程直至当前枚举项为空实现对集合的遍历。
Reset()将当前枚举项的引用重置到初始状态。
从头到尾对一个集合进行枚举本质上并不是一个线程安全的过程,即使一个集合本身是线程安全的,其他线程仍可以修改该集合,这将导致枚举数引发异常。如果说写七八年代码的连这都不懂,我信(被前前同事坑怕了,中毒太深)。
若要在枚举过程中保证线程安全,可以在整个枚举过程中锁定集合。
2.3 ICollection
[ComVisible(true)] public interface ICollection : IEnumerable { int Count { get; } bool IsSynchronized { get; } object SyncRoot { get; } void CopyTo(Array array, int index); }
各种实现类中,有几个比较关键的属性:
Capacity属性表示的是集合的容量。
Count属性表示的是集合中当前数据项个数,按照各自的规则自动扩容。
Clear操作只是清空集合中的元素,但集合的容量没变,仍然占用着内存空间,类似于乘客从列车中出来,但列车的容量不变。
SyncRoot属性主要在需要线程安全的操作时使用。
2.4 IDictionary
属于一对一的字典,Keys和Values独立存储,通过对Key执行哈希获取Value的存储位置,所以Key可以是非null的任意类型,且不能有重复。
[ComVisible(true)] [DefaultMember("Item")] public interface IDictionary : ICollection, IEnumerable { object this[object key] { get; set; } bool IsReadOnly { get; } ICollection Keys { get; } ICollection Values { get; } void Add(object key, object value); void Clear(); bool Contains(object key); IDictionaryEnumerator GetEnumerator(); void Remove(object key); }
在具体的实现类中:
Dictionary<TKey,TValue>采用链地址法处理哈希冲突,填充因子是1。
ConcurrentDictionary<TKey,TValue>线程安全的键值集合,TyrAdd、TryGetValue、TryRemove、TryUpdate方法以非阻塞的方式访问元素。
SortedList<TKey,TValue>用两个数组分别存储键和值并保持大小的同步性,所以对GC比较友好一点,且支持用索引或键查找值并按照键排序。
SortedDictionary<TKey,TValue>是一个按照键构造的二叉查找树,所以,添加或删除操作只会根据红黑树的特性旋转节点来保持平衡故性能优于SortedList而差于Dictionary,而查找时可利用二分法,结构上对GC而言相对比较复杂。
其中,Dictionary[key]=value相当于AddOrUpdate,属于引用替换,亲们可以考虑下是不是线程安全的。
2.5 ILookup
属于一对多的字典类型。
[DefaultMember("Item")] public interface ILookup<TKey, TElement> : IEnumerable<IGrouping<TKey, TElement>>, IEnumerable { IEnumerable<TElement> this[TKey key] { get; } int Count { get; } bool Contains(TKey key); }
2.6 IList
提供对数据项的增删操作,可按照索引访问数据项。
[ComVisible(true)] [DefaultMember("Item")] public interface IList : ICollection, IEnumerable { object this[int index] { get; set; } bool IsFixedSize { get; } bool IsReadOnly { get; } int Add(object value); void Clear(); bool Contains(object value); int IndexOf(object value); void Insert(int index, object value); void Remove(object value); void RemoveAt(int index); }
索引符是一种特殊类型的属性,提供按照索引访问数据项的功能。
遍历删除的操作,可以考虑使用倒叙方式。
2.7 ISet
不允许有重复的元素。
public interface ISet<T> : ICollection<T>, IEnumerable<T>, IEnumerable { bool Add(T item); void ExceptWith(IEnumerable<T> other); void IntersectWith(IEnumerable<T> other); bool IsProperSubsetOf(IEnumerable<T> other); bool IsProperSupersetOf(IEnumerable<T> other); bool IsSubsetOf(IEnumerable<T> other); bool IsSupersetOf(IEnumerable<T> other); bool Overlaps(IEnumerable<T> other); bool SetEquals(IEnumerable<T> other); void SymmetricExceptWith(IEnumerable<T> other); void UnionWith(IEnumerable<T> other); }
这里牢骚几句,好多程序员处理去重时,都用dictionary(键值一样),明明有set不用,真捉急。
还有一些场景,比如电商产品起价的更新时,需要对数据取交集、并集、差集的运算,此时set也很有妙用,可以将复杂度从N^2降低到N。