泛型是什么
泛型是C# 2.0版本新增加的功能,泛型将“类型参数”的概念引入了.Net FrameWork中,使开发者可以将类型作为参数使用,进一步提高逻辑代码的复用。
我们都知道面向对象编程的一大优点便是代码的模块化及高复用性,但我们利用不同类型使用同一套逻辑代码时往往需要将目标类型隐式转化为object类型来操作,频繁的强制转换实际上会带了很大的性能开销;object是引用类型,而当我们想用值类型来使用这套代码时,值类型转化为引用类型还会引起“装箱”操作,带了性能上的损耗。类型的转换往往还伴随着未知的风险。
举个例子,我们定制一个集合类,功能是添加和删除集合中的对象。如果我们定制类的时候将参数定制为object类型,显然会带来上述的隐患,我们希望能将此集合类应用于各个类型,由此,泛型应运而生。我们在定制集合类的时候只需将其定义为T类型的集合(在下面的例子中会以例子说明,T泛指任意类型),而在实例集合类时我们为其指定我们需要的类型,比如我们需要string类就将其类型参数指定为string,那么一个string类型的集合类实例就创建成功了,我们只能在此集合类的实例中操作string类型的对象。这样既保证了类型安全,又减少了性能开销。泛型将运行时的安全检查转移到了编译时,提高了代码效率。
泛型是万能的?不是所有的逻辑代码都可以被各种类型复用!比如我们通常将类型分为引用类型和值类型,在这两大种类型中,有些特性可能是不互通的,或者说,自己定制了一套逻辑代码,这套代码只能允许值类型作为类型参数,而不适用于引用类型,那么我们就要为类型参数做一些限制,以防使用这套逻辑代码时制定一个不符合要求的类型参数,这就涉及到使用类型约束来规避这样的风险。
泛型优点
通过上面的概述可以显而易见的发现如下优点:
·可以最大限度的保证代码的复用,提高代码效率;
·显式的指定类型,保证了类型安全,规避了类型转换带来的性能消耗和未知风险;
·可以指定为值类型,避免值类型转换为引用类型产生的装箱操作,降低性能消耗;
·提高了代码的性能;
·使代码更加易于维护和拓展。
常见泛型
类
List<T>
我们经常用List来创建一个集合,它类似于数组,拥有索引;还可以利用Add和Remove等方法动态添加和删除集合中的对象。示例如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GenericClass
{
class Program
{
static void Main(string[] args)
{
//值类型的List的集合 此时参数T是int
List<int> myInts = new List<int>() { 1, 2, 4, 6, 7 };
foreach (int i in myInts)
{
Console.Write(i + " ");
}
Console.WriteLine();
//引用类型的List的集合 此时参数T是string
List<string> myStrings = new List<string> { "aaa", "bbb", "ccc" };
foreach (string s in myStrings)
{
Console.Write(s + " ");
}
Console.WriteLine();
//此行代码会报错,因为ddd是string型而不是int型
myInts.Add("ddd");
}
}
}
Dictionary<TKey,TValue>
还有个常用的泛型类是字典类,它需要指定Key的类型参数和Value的类型参数,字典的Key与Value是映射关系,可以利用字典来实现某些特定的查询功能。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GenericClass
{
class Program
{
static void Main(string[] args)
{
//创建字典,key为int型,value为string型
Dictionary<int, string> dic
= new Dictionary<int, string>()
{
{000101,"AAA"},
{000102,"BBB"},
{000103,"CCC"}
};
//遍历字典,可用var来推导字典元素的类型
//其实同样也为泛型,由于字典被指定为int与string的类型组合
//所以元素的类型组合也为int与string类型
foreach (var dicElement in dic)
{
Console.WriteLine("Key: " + dicElement.Key + ", Value: " + dicElement.Value);
}
//还可以写成这样 注:如果类型参数不匹配将会报错
foreach (KeyValuePair<int, string> dicElement in dic)
{
Console.WriteLine("Key: " + dicElement.Key + ", Value: " + dicElement.Value);
}
}
}
}
我们还可以自定义一个泛型类来提供我们使用
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GenericClass
{
public class Test<T>
{
public Test()
{
//打印该类型实例初始的值
Console.WriteLine(default(T));
}
}
class Program
{
static void Main(string[] args)
{
//打印0
Test<int> t1 = new Test<int>();
//打印空的字符串
Test<string> t2 = new Test<string>();
//打印False
Test<bool> t3 = new Test<bool>();
}
}
}
方法
常见的泛型方法是非泛型类中Array类中的静态方法Sort方法,它有多个重载方法,其中比较常用的是对指定类型数组进行排序。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GenericClass
{
class Program
{
static void Main(string[] args)
{
//创建一个int类型无序数组
int[] myInts = new int[] { 1, 8, 2, 6, 4, 5, 2 };
//指定方法传参的类型,即类型参数指定为int,然后将myInts数组传入
Array.Sort<int>(myInts);
foreach (int i in myInts)
{
Console.Write(i + " ");
}
Console.WriteLine();
}
}
}
我们还可以定义自己的泛型方法,下述代码实现了返回参数的字符串形式
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GenericClass
{
class Program
{
static void Main(string[] args)
{
float f = 9.81f;
//Value is 9.81
Console.WriteLine(GetString<float>(f));
int i = 10;
//Value is 10
Console.WriteLine(GetString<int>(i));
bool b = true;
//Value is True
Console.WriteLine(GetString<bool>(b));
object obj = new object();
//Value is System.Object
Console.WriteLine(GetString<object>(obj));
}
static string GetString<TInput>(TInput input)
{
return "Value is " + input.ToString();
}
}
}
还可以使用泛型类中的泛型方法,与委托一起介绍。
委托
要用到的Converter委托的API说明:
namespace System
{
// 摘要:
// 表示将对象从一种类型转换为另一种类型的方法。
//
// 参数:
// input:
// 要转换的对象。
//
// 类型参数:
// TInput:
// 要转换的对象的类型。
//
// TOutput:
// 要将输入对象转换到的类型。
//
// 返回结果:
// TOutput,它表示已转换的 TInput。
public delegate TOutput Converter<in TInput, out TOutput>(TInput input);
}
//需要用到的泛型方法的官方API介绍,包含于List<T>中,接收<T,TOutput>的泛型委托,返回TOutput类型的泛型数组
// 摘要:
// 将当前 System.Collections.Generic.List<T> 中的元素转换为另一种类型,并返回包含转换后的元素的列表。
//
// 参数:
// converter:
// 将每个元素从一种类型转换为另一种类型的 System.Converter<TInput,TOutput> 委托。
//
// 类型参数:
// TOutput:
// 目标数组元素的类型。
//
// 返回结果:
// 目标类型的 System.Collections.Generic.List<T>,其中包含当前 System.Collections.Generic.List<T>
// 中的转换后的元素。
//
// 异常:
// System.ArgumentNullException:
// converter 为 null。
public List<TOutput> ConvertAll<TOutput>(Converter<T, TOutput> converter);
下面例子是将int型泛型数组转换为string型泛型数组的具体例子,应用以上介绍的泛型委托和泛型方法:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GenericClass
{
class Program
{
static void Main(string[] args)
{
List<int> myInts = new List<int>()
{
1,2,3,4,123,777
};
//将方法委托给泛型委托,指定输入类型为int,输出类型为string
Converter<int, string> converter = IntToString;
//ConvertAll方法返回List<T> 这里我们需要List<string>
//所以类型参数指定为string,方法参数为Converter委托的实例converter
List<string> results = myInts.ConvertAll<string>(converter);
foreach (string s in results)
{
Console.WriteLine(s);
}
}
//将int值转换为字符串的方法
static string IntToString(int val)
{
return "Int value is " + val.ToString();
}
}
}
接口
还有常用到的泛型接口。前面提到过的Sort方法在泛型类中,由于创建泛型类实例时就已经指定了类型,所以Sort方法无需再指定类型,但是不是任何类型都可以使用Sort方法,例如下面这段代码就会抛出异常:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GenericClass
{
class Program
{
static void Main(string[] args)
{
Program[] ps = new Program[]
{
new Program(),
new Program(),
new Program(),
new Program(),
new Program(),
};
Array.Sort<Program>(ps);
foreach (var p in ps)
{
Console.WriteLine(p);
}
}
}
}
异常:未能比较数组中的两个元素,必须至少一个对象实现IComparable。
显然,我们使用的Program类的确没有实现IComparable接口,实现这个接口我们才能实现定制类的排序(比较)方式。那在【方法】模块中为什么能对int数组直接使用Sort方法呢?是因为int类型已经实现了该接口,我们可以在int类型的定义中看到。
public struct Int32 : IComparable, IFormattable, IConvertible, IComparable<int>, IEquatable<int>
所以需要自己定制类继承并实现IComparable接口,以便于使用Sort方法,示例如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GenericClass
{
public class Person : IComparable<Person>
{
public readonly string Name;
public readonly int Age;
public Person(string name, int age)
{
Name = name;
Age = age;
}
//实现泛型接口
public int CompareTo(Person other)
{
if(other==null)
{
return 1;
}
return this.Age - other.Age;
}
}
class Program
{
static void Main(string[] args)
{
//写法1 利用Array类的泛型方法Sort
Person[] ps1 = new Person[]
{
new Person("AAA",31),
new Person("BBB",17),
new Person("CCC",25),
new Person("DDD",12)
};
Array.Sort<Person>(ps1);
foreach (Person p in ps1)
{
Console.WriteLine("Name: " + p.Name + ", Age: " + p.Age);
}
//写法2 创建Person的泛型集合 调用Sort方法
List<Person> ps2 = new List<Person>()
{
new Person("AAA",31),
new Person("BBB",17),
new Person("CCC",25),
new Person("DDD",12)
};
ps2.Sort();
foreach (Person p in ps2)
{
Console.WriteLine("Name: " + p.Name + ", Age: " + p.Age);
}
}
}
}
不显式的指定接口类型,而是指定为T类型,则写成如下模式:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GenericClass
{
public class Person<T> : IComparable<T>
{
public readonly T Info;
public Person(T Info)
{
//......
}
public int CompareTo(T other)
{
//......
return 0;
}
}
}
类似的我们可以把信息封装成一个类,然后在实例该类对象时指定类型参数,为特定的类型提供运作方式。但显然在这个例子中这样定义是不合适的,我们无法根据未知的T类型来具体的实现ICompareble的接口函数CompareTo。这里仅仅提供一个定义无指定类型参数的实现泛型接口的方式。
泛型约束
由于不是所有逻辑代码都可以被任何类复用,所以我们在定制泛型类、方法、委托和接口等时,一般要对泛型参数添加约束。使用class关键字约束泛型只允许引用类型的类型参数;使用struct关键字约束泛型只允许值类型的类型参数;约定T类型实现的接口,只能指定实现特定接口的类型;使用new()来约束泛型的类型参数必须含有无参数构造函数;也可以同时使用上述条件的某几个进行组合,但进行组合的条件不能相互冲突,且与继承相似,可以限定多个接口单只能限定一个类;可以为多个类型参数都加上限制条件。
1.值类型一般分为结构类型和枚举类型,内置的整型、浮点型、bool型和decimal以及用户自定的枚举和结构均为可用的类型。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GenericClass
{
public class Test<T>
where T : struct
{
}
public enum MyEnum { }
public struct MyStruct { }
public class MyClass { }
class Program
{
static void Main(string[] args)
{
Test<int> t1 = new Test<int>();
Test<MyEnum> t2 = new Test<MyEnum>();
Test<MyStruct> t3 = new Test<MyStruct>();
//编译器提示错误
Test<object> t4 = new Test<object>();
//编译器提示错误
Test<MyClass> t4 = new Test<MyClass>();
}
}
}
2.内置的dynamic、object、string为可用的引用类型,定制的类类型也为可用的引用类型。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GenericClass
{
public class Test<T>
where T : class
{
}
class Program
{
static void Main(string[] args)
{
Test<object> t1 = new Test<object>();
Test<dynamic> t2 = new Test<dynamic>();
Test<string> t3 = new Test<string>();
//编译器会提示错误
Test<int> t4 = new Test<int>();
//编译器会提示错误
Test<float> t5 = new Test<float>();
}
}
}
3.也可以具体到某种类型,限定T只能是该类或者继承自该类。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GenericClass
{
public class Fruit { }
public class Apple : Fruit { }
public class Fuji : Apple { }
public class Test<T>
where T : Fruit
{
}
class Program
{
static void Main(string[] args)
{
Test<Fruit> t1 = new Test<Fruit>();
Test<Apple> t2 = new Test<Apple>();
Test<Fuji> t3 = new Test<Fuji>();
//编译器会提示错误
Test<int> t4 = new Test<int>();
//编译器会提示错误
Test<object> t5 = new Test<object>();
}
}
}
4.指定类型参数必须实现某接口。接口约束可以看做与基类约束类似。类型参数指定的类型需要满足通过隐式转换或者装箱操作转换为限定的基类或者接口,否则此类型为非法类型。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GenericClass
{
public class Test<T>
where T : IComparable<T>
{
}
public struct MyStruct { }
public class MyClass : IComparable<MyClass>
{
public int CompareTo(MyClass other)
{
throw new NotImplementedException();
}
}
class Program
{
static void Main(string[] args)
{
Test<int> t1 = new Test<int>();
Test<string> t2 = new Test<string>();
Test<MyClass> t3 = new Test<MyClass>();
//编译器提示错误
Test<object> t4 = new Test<object>();
//编译器提示错误
Test<MyStruct> t5 = new Test<MyStruct>();
}
}
}
5.使用new()来约束T类型具有无参数的构造函数,内置的string类型没有无参数的构造函数,结构(值)类型符合无参数构造函数这一条件。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GenericClass
{
public class MyClass1
{
public readonly string ClassName;
public MyClass1()
{
ClassName = "MyClass1";
}
}
class Program
{
static void Main(string[] args)
{
int t1 = CreateInstance<int>();
//t1 value is 0
Console.WriteLine("t1 value is " + t1);
MyClass1 mc1 = CreateInstance<MyClass1>();
//mc1 class named MyClass1
Console.WriteLine("mc1 class named " + mc1.ClassName);
//编译器报错
string s1 = CreateInstance<string>();
}
//定义泛型方法,用于创建类型实例,指定类型必须有无构造参数的构造器
static T CreateInstance<T>()
where T : new()
{
return new T();
}
}
}
6.也可以同时约束多个条件,前提是各个条件不能冲突。
public class MyClass<T>
where T : class, IComparable<T>, new()
{
}
7.也可以为各个类型都指定约束。
public class MyClass<T, TInput, TOut>
where T : class
where TInput : struct
where TOut : class,IComparable<TInput>
{
}
总结
写了这么多,并在每个小知识点上都加了示例代码,个人感觉收获还是不少的。从对泛型懵懂的认识(当时自己管这个东西叫动态数组,只当做数组来用,对泛型这个概念几乎为0)到如今能写出一篇关于泛型基础内容,自我的提升感是他人无法体会的。
泛型是C#新引入的功能,相对于旧有的编程模式来说极大地提升了程序的效率、安全性和可维护性,简化了开发人员的工作,使开发人员更加专注的实现需求。
本篇涉及到的只是泛型入门所需掌握的基础知识,作为个人的学习记录,由于对泛型也是一知半解,篇幅较长,难免出错,还请相关专业领域的读者能提出批评和建议。
参考资料
[1]. 《Unity3D脚本编程——使用C#语言开发跨平台游戏》 陈嘉栋 著。
[2]. https://msdn.microsoft.com/zh-cn/library/512aeb7t.aspx C#编程指南(泛型)。