时间:2021-05-20
我们都知道泛型在C#的重要性,泛型是OOP语言中三大特征的多态的最重要的体现,几乎泛型撑起了整个.NET框架,在讲泛型之前,我们可以抛出一个问题,我们现在需要一个可扩容的数组类,且满足所有类型,不管是值类型还是引用类型,那么在没有用泛型方法实现,如何实现?
我们肯定会想到用object来作为类型参数,因为在C#中,所有类型都是基于Object类型的。因此Object是所有类型的最基类,那么我们的可扩容数组类如下:
public class ArrayExpandable { private object?[] _items = null; private int _defaultCapacity = 4; private int _size; public object? this[int index] { get { if (index < 0 || index >= _size) throw new ArgumentOutOfRangeException(nameof(index)); return _items[index]; } set { if (index < 0 || index >= _size) throw new ArgumentOutOfRangeException(nameof(index)); _items[index] = value; } } public int Capacity { get => _items.Length; set { if (value < _size) { throw new ArgumentOutOfRangeException(nameof(value)); } if (value != _items.Length) { if (value > 0) { object[] newItems = new object[value]; if (_size > 0) { Array.Copy(_items, newItems, _size); } _items = newItems; } else { _items = new object[_defaultCapacity]; } } } } public int Count => _size; public ArrayExpandable() { _items = new object?[0]; } public ArrayExpandable(int capacity) { _items = new object?[capacity]; } public void Add(object? value) { //数组元素为0或者数组元素容量满 if (_size == _items.Length) EnsuresCapacity(_size + 1); _items[_size] = value; _size++; } private void EnsuresCapacity(int size) { if (_items.Length < size) { int newCapacity = _items.Length == 0 ? _defaultCapacity : _items.Length * 2; if (newCapacity < size) newCapacity = size; Capacity = newCapacity; } }然后我们来验证下:
var arrayStr = new ArrayExpandable();var strs = new string[] { "ryzen", "reed", "wymen" };for (int i = 0; i < strs.Length; i++){ arrayStr.Add(strs[i]); string value = (string)arrayStr[i];//改为int value = (int)arrayStr[i] 运行时报错 Console.WriteLine(value);}Console.WriteLine($"Now {nameof(arrayStr)} Capacity:{arrayStr.Capacity}");var array = new ArrayExpandable();for (int i = 0; i < 5; i++){ array.Add(i); int value = (int)array[i]; Console.WriteLine(value);}Console.WriteLine($"Now {nameof(array)} Capacity:{array.Capacity}");输出:
Copy
ryzen
reed
wymen
gavin
Now arrayStr Capacity:4
0
1
2
3
4
Now array Capacity:8
貌似输出结果是正确的,能够动态进行扩容,同样的支持值类型Struct的int32和引用类型的字符串,但是其实这里会发现一些问题,那就是
大致执行模型如下:
引用类型:
值类型:
那么有没有一种方法能够避免上面遇到的三种问题呢?在借鉴了cpp的模板和java的泛型经验,在C#2.0的时候推出了更适合.NET体系下的泛型
那么测试代码则改写为如下:
var arrayStr = new ArrayExpandable<string>();var strs = new string[] { "ryzen", "reed", "wymen", "gavin" };for (int i = 0; i < strs.Length; i++){ arrayStr.Add(strs[i]); string value = arrayStr[i];//改为int value = arrayStr[i] 编译报错 Console.WriteLine(value);}Console.WriteLine($"Now {nameof(arrayStr)} Capacity:{arrayStr.Capacity}");var array = new ArrayExpandable<int>();for (int i = 0; i < 5; i++){ array.Add(i); int value = array[i]; Console.WriteLine(value);}Console.WriteLine($"Now {nameof(array)} Capacity:{array.Capacity}");输出:
Copy
ryzen
reed
wymen
gavin
Now arrayStr Capacity:4
0
1
2
3
4
Now array Capacity:8
我们通过截取部分ArrayExpandable<T>的IL查看其本质是个啥:
//声明类.class public auto ansi beforefieldinit MetaTest.ArrayExpandable`1<T> extends [System.Runtime]System.Object{ .custom instance void [System.Runtime]System.Reflection.DefaultMemberAttribute::.ctor(string) = ( 01 00 04 49 74 65 6D 00 00 ) } //Add方法.method public hidebysig instance void Add(!T 'value') cil managed{ // 代码大小 69 (0x45) .maxstack 3 .locals init (bool V_0) IL_0000: nop IL_0001: ldarg.0 IL_0002: ldfld int32 class MetaTest.ArrayExpandable`1<!T>::_size IL_0007: ldarg.0 IL_0008: ldfld !0[] class MetaTest.ArrayExpandable`1<!T>::_items IL_000d: ldlen IL_000e: conv.i4 IL_000f: ceq IL_0011: stloc.0 IL_0012: ldloc.0 IL_0013: brfalse.s IL_0024 IL_0015: ldarg.0 IL_0016: ldarg.0 IL_0017: ldfld int32 class MetaTest.ArrayExpandable`1<!T>::_size IL_001c: ldc.i4.1 IL_001d: add IL_001e: call instance void class MetaTest.ArrayExpandable`1<!T>::EnsuresCapacity(int32) IL_0023: nop IL_0024: ldarg.0 IL_0025: ldfld !0[] class MetaTest.ArrayExpandable`1<!T>::_items IL_002a: ldarg.0 IL_002b: ldfld int32 class MetaTest.ArrayExpandable`1<!T>::_size IL_0030: ldarg.1 IL_0031: stelem !T IL_0036: ldarg.0 IL_0037: ldarg.0 IL_0038: ldfld int32 class MetaTest.ArrayExpandable`1<!T>::_size IL_003d: ldc.i4.1 IL_003e: add IL_003f: stfld int32 class MetaTest.ArrayExpandable`1<!T>::_size IL_0044: ret} // end of method ArrayExpandable`1::Add原来定义的时候就是用了个T作为占位符,起一个模板的作用,我们对其实例化类型参数的时候,补足那个占位符,我们可以在编译期就知道了其类型,且不用在运行时进行类型检测,而我们也可以对比ArrayExpandable和ArrayExpandable<T>在类型为值类型中的IL,查看是否进行拆箱和装箱操作,以下为IL截取部分:
ArrayExpandable:
IL_0084: newobj instance void GenericSample.ArrayExpandable::.ctor() IL_0089: stloc.2 IL_008a: ldc.i4.0 IL_008b: stloc.s V_6 IL_008d: br.s IL_00bc IL_008f: nop IL_0090: ldloc.2 IL_0091: ldloc.s V_6 IL_0093: box [System.Runtime]System.Int32 //box为装箱操作 IL_0098: callvirt instance void GenericSample.ArrayExpandable::Add(object) IL_009d: nop IL_009e: ldloc.2 IL_009f: ldloc.s V_6 IL_00a1: callvirt instance object GenericSample.ArrayExpandable::get_Item(int32) IL_00a6: unbox.any [System.Runtime]System.Int32 //unbox为拆箱操作ArrayExpandable:
IL_007f: newobj instance void class GenericSample.ArrayExpandable`1<int32>::.ctor() IL_0084: stloc.2 IL_0085: ldc.i4.0 IL_0086: stloc.s V_6 IL_0088: br.s IL_00ad IL_008a: nop IL_008b: ldloc.2 IL_008c: ldloc.s V_6 IL_008e: callvirt instance void class GenericSample.ArrayExpandable`1<int32>::Add(!0) IL_0093: nop IL_0094: ldloc.2 IL_0095: ldloc.s V_6 IL_0097: callvirt instance !0 class GenericSample.ArrayExpandable`1<int32>::get_Item(int32)我们从IL也能看的出来,ArrayExpandable<T>的T作为一个类型参数,在编译后在IL已经确定了其类型,因此当然也就不存在装拆箱的情况,在编译期的时候IDE能够检测类型,因此也就不用在运行时进行类型检测,但并不代表不能通过运行时检测类型(可通过is和as),还能通过反射体现出泛型的灵活性,后面会讲到
其实有了解ArrayList和List的朋友就知道,ArrayExpandable和ArrayExpandable<T>其实现大致就是和它们一样,只是简化了很多的版本,我们这里可以通过 BenchmarkDotNet 来测试其性能对比,代码如下:
[SimpleJob(RuntimeMoniker.NetCoreApp31,baseline:true)] [SimpleJob(RuntimeMoniker.NetCoreApp50)] [MemoryDiagnoser] public class TestClass { [Benchmark] public void EnumAE_ValueType() { ArrayExpandable array = new ArrayExpandable(); for (int i = 0; i < 10000; i++) { array.Add(i);//装箱 int value = (int)array[i];//拆箱 } array = null;//确保进行垃圾回收 } [Benchmark] public void EnumAE_RefType() { ArrayExpandable array = new ArrayExpandable(); for (int i = 0; i < 10000; i++) { array.Add("r"); string value = (string)array[i]; } array = null;//确保进行垃圾回收 } [Benchmark] public void EnumAE_Gen_ValueType() { ArrayExpandable<int> array = new ArrayExpandable<int>(); for (int i = 0; i < 10000; i++) { array.Add(i); int value = array[i]; } array = null;//确保进行垃圾回收; } [Benchmark] public void EnumAE_Gen_RefType() { ArrayExpandable<string> array = new ArrayExpandable<string>(); for (int i = 0; i < 10000; i++) { array.Add("r"); string value = array[i]; } array = null;//确保进行垃圾回收; } [Benchmark] public void EnumList_ValueType() { List<int> array = new List<int>(); for (int i = 0; i < 10000; i++) { array.Add(i); int value = array[i]; } array = null;//确保进行垃圾回收; } [Benchmark] public void EnumList_RefType() { List<string> array = new List<string>(); for (int i = 0; i < 10000; i++) { array.Add("r"); string value = array[i]; } array = null;//确保进行垃圾回收; } [Benchmark(Baseline =true)] public void EnumAraayList_valueType() { ArrayList array = new ArrayList(); for (int i = 0; i < 10000; i++) { array.Add(i); int value = (int)array[i]; } array = null;//确保进行垃圾回收; } [Benchmark] public void EnumAraayList_RefType() { ArrayList array = new ArrayList(); for (int i = 0; i < 10000; i++) { array.Add("r"); string value = (string)array[i]; } array = null;//确保进行垃圾回收; } }我还加入了.NETCore3.1和.NET5的对比,且以.NETCore3.1的EnumAraayList_valueType方法为基准,性能测试结果如下:
用更直观的柱形图来呈现:
我们能看到在这里List的性能在引用类型和值类型中都是所以当中是最好的,不管是执行时间、GC次数,分配的内存空间大小,都是最优的,同时.NET5在几乎所有的方法中性能都是优于.NETCore3.1,这里还提一句,我实现的ArrayExpandable和ArrayExpandable<T>性能都差于ArrayList和List,我还没实现IList和各种方法,只能说句dotnet基金会牛逼
类、结构、接口、方法、和委托可以声明一个或者多个类型参数,我们直接看代码:
interface IFoo<InterfaceT>{ void InterfaceMenthod(InterfaceT interfaceT);}class Foo<ClassT, ClassT1>: IFoo<StringBuilder>{ public ClassT1 Field; public delegate void MyDelegate<DelegateT>(DelegateT delegateT); public void DelegateMenthod<DelegateT>(DelegateT delegateT, MyDelegate<DelegateT> myDelegate) { myDelegate(delegateT); } public static string operator +(Foo<ClassT, ClassT1> foo,string s) { return $"{s}:{foo.GetType().Name}"; } public List<ClassT> Property{ get; set; } public ClassT1 Property1 { get; set; } public ClassT this[int index] => Property[index];//没判断越界 public Foo(List<ClassT> classT, ClassT1 classT1) { Property = classT; Property1 = classT1; Field = classT1; Console.WriteLine($"构造函数:parameter1 type:{Property.GetType().Name},parameter2 type:{Property1.GetType().Name}"); } //方法声明了多个新的类型参数 public void Method<MenthodT, MenthodT1>(MenthodT menthodT, MenthodT1 menthodT1) { Console.WriteLine($"Method<MenthodT, MenthodT1>:{(menthodT.GetType().Name)}:{menthodT.ToString()}," + $"{menthodT1.GetType().Name}:{menthodT1.ToString()}"); } public void Method(ClassT classT) { Console.WriteLine($"{nameof(Method)}:{classT.GetType().Name}:classT?.ToString()"); } public void InterfaceMenthod(StringBuilder interfaceT) { Console.WriteLine(interfaceT.ToString()); }}控制台测试代码:
static void Main(string[] args){ Test(); Console.ReadLine();}static void Test(){ var list = new List<int>() { 1, 2, 3, 4 }; var foo = new Foo<int, string>(list, "ryzen"); var index = 0; Console.WriteLine($"索引:索引{index}的值:{foo[index]}"); Console.WriteLine($"Filed:{foo.Field}"); foo.Method(2333); foo.Method<DateTime, long>(DateTime.Now, 2021); foo.DelegateMenthod<string>("this is a delegate", DelegateMenthod); foo.InterfaceMenthod(new StringBuilder().Append("InterfaceMenthod:this is a interfaceMthod")); Console.WriteLine(foo+"重载+运算符");}static void DelegateMenthod(string str){ Console.WriteLine($"{nameof(DelegateMenthod)}:{str}");}输出如下:
构造函数:parameter1 type:List`1,parameter2 type:String
索引:索引0的值:1
Filed:ryzen
Method:Int32:classT?.ToString()
Method<MenthodT, MenthodT1>:DateTime:2021/03/02 11:45:40,Int64:2021
DelegateMenthod:this is a delegate
InterfaceMenthod:this is a interfaceMthod
重载+运算符:Foo`2
我们通过例子可以看到的是:
父类和实现类或接口的接口都可以是实例化类型,直接看代码:
interface IFooBase<IBaseT>{}interface IFoo<InterfaceT>: IFooBase<string>{ void InterfaceMenthod(InterfaceT interfaceT);}class FooBase<ClassT>{}class Foo<ClassT, ClassT1>: FooBase<ClassT>,IFoo<StringBuilder>{}我们可以通过例子看出:
我们定义如下一个类和一个方法,且不会报错:
class D<T> { } class C<T> : D<C<C<T>>> { void Foo() { var foo = new C<C<T>>(); Console.WriteLine(foo.ToString()); } }因为T能在实例化的时候确定其类型,因此也支持这种循环套用自己的类和方法的定义
我们先上代码:
class FooBase{ } class Foo : FooBase { } class someClass<T,K> where T:struct where K :FooBase,new() { } static void TestConstraint() { var someClass = new someClass<int, Foo>();//通过编译 //var someClass = new someClass<string, Foo>();//编译失败,string不是struct类型 //var someClass = new someClass<string, long>();//编译失败,long不是FooBase类型 }再改动下Foo类:
class Foo : FooBase { public Foo(string str) { }}static void TestConstraint(){ var someClass = new someClass<int, Foo>();//编译失败,因为new()约束必须类含有一个无参构造器,可以再给Foo类加上个无参构造器就能编译通过}我们可以看到,通过where语句,可以对类型参数进行约束,而且一个类型参数支持多个约束条件(例如K),使其在实例化类型参数的时候,必须按照约束的条件对应实例符合条件的类型,而where条件约束的作用就是起在编译期约束类型参数的作用
说到out和in之前,我们可以说下协变和逆变,在C#中,只有泛型接口和泛型委托可以支持协变和逆变
我们先看下代码:
class FooBase{ }class Foo : FooBase {}interface IBar<T> { T GetValue(T t);}class Bar<T> : IBar<T>{ public T GetValue(T t) { return t; }}static void Test(){ var foo = new Foo(); FooBase fooBase = foo;//编译成功 IBar<Foo> bar = new Bar<Foo>(); IBar<FooBase> bar1 = bar;//编译失败 }这时候你可能会有点奇怪,为啥那段代码会编译失败,明明Foo类可以隐式转为FooBase,但作为泛型接口类型参数实例化却并不能呢?使用out约束泛型接口IBar的T,那段代码就会编译正常,但是会引出另外一段编译报错:
interface IBar<out T> { T GetValue(string str);//编译成功 //T GetValue(T t);//编译失败 T不能作为形参输入,用out约束T支持协变,T可以作为返回值输出 }IBar<Foo> bar = new Bar<Foo>();IBar<FooBase> bar1 = bar;//编译正常因此我们可以得出以下结论:
而支持迭代的泛型接口IEnumerable也是这么定义的:
public interface IEnumerable<out T> : IEnumerable { new IEnumerator<T> GetEnumerator(); }我们将上面代码改下:
class FooBase{ }class Foo : FooBase {}interface IBar<T> { T GetValue(T t);}class Bar<T> : IBar<T>{ public T GetValue(T t) { return t; }}static void Test1(){ var fooBase = new FooBase(); Foo foo = (Foo)fooBase;//编译通过,运行时报错 IBar<FooBase> bar = new Bar<FooBase>(); IBar<Foo> bar1 = (IBar<Foo>)bar;//编译通过,运行时报错}我们再改动下IBar,发现出现另外一处编译失败
interface IBar<in T> { void GetValue(T t);//编译成功 //T GetValue(T t);//编译失败 T不能作为返回值输出,用in约束T支持逆变,T可以作为返回值输出} IBar<FooBase> bar = new Bar<FooBase>(); IBar<Foo> bar1 = (IBar<Foo>)bar;//编译通过,运行时不报错 IBar<Foo> bar1 = bar;//编译通过,运行时不报错因此我们可以得出以下结论:
同样的泛型委托Action就是个逆变的例子:
public delegate void Action<in T>(T obj);我们先来看看以下代码:
static void Main(string[] args){ var lsInt = new ArrayExpandable<int>(); lsInt.Add(1); var lsStr = new ArrayExpandable<string>(); lsStr.Add("ryzen"); var lsStr1 = new ArrayExpandable<string>(); lsStr.Add("ryzen");}然后通过ildasm查看其IL,开启视图-》显示标记值,查看Main方法:
void Main(string[] args) cil managed{ .entrypoint // 代码大小 52 (0x34) .maxstack 2 .locals init (class MetaTest.ArrayExpandable`1<int32> V_0, class MetaTest.ArrayExpandable`1<string> V_1, class MetaTest.ArrayExpandable`1<string> V_2) IL_0000: nop IL_0001: newobj instance void class MetaTest.ArrayExpandable`1<int32>::.ctor() IL_0006: stloc.0 IL_0007: ldloc.0 IL_0008: ldc.i4.1 IL_0009: callvirt instance void class MetaTest.ArrayExpandable`1<int32>::Add(!0) IL_000e: nop IL_000f: newobj instance void class MetaTest.ArrayExpandable`1<string>::.ctor() IL_0014: stloc.1 IL_0015: ldloc.1 IL_0016: ldstr "ryzen" IL_001b: callvirt instance void class MetaTest.ArrayExpandable`1<string>::Add(!0) IL_0020: nop IL_0021: newobj instance void class MetaTest.ArrayExpandable`1<string>::.ctor() IL_0026: stloc.2 IL_0027: ldloc.1 IL_0028: ldstr "ryzen" IL_002d: callvirt instance void class MetaTest.ArrayExpandable`1<string>::Add(!0) IL_0032: nop IL_0033: ret} // end of method Program::Main打开元数据表将上面所涉及到的元数据定义表和类型规格表列出:
metainfo:
-----------定义部分TypeDef #2 (02000003)------------------------------------------------------- TypDefName: MetaTest.ArrayExpandable`1 (02000003) Flags : [Public] [AutoLayout] [Class] [AnsiClass] [BeforeFieldInit] (00100001) Extends : 0100000C [TypeRef] System.Object 1 Generic Parameters (0) GenericParamToken : (2a000001) Name : T flags: 00000000 Owner: 02000003 Method #8 (0600000a) ------------------------------------------------------- MethodName: Add (0600000A) Flags : [Public] [HideBySig] [ReuseSlot] (00000086) RVA : 0x000021f4 ImplFlags : [IL] [Managed] (00000000) CallCnvntn: [DEFAULT] hasThis ReturnType: Void 1 Arguments Argument #1: Var!0 1 Parameters (1) ParamToken : (08000007) Name : value flags: [none] (00000000) ------类型规格部分TypeSpec #1 (1b000001)------------------------------------------------------- TypeSpec : GenericInst Class MetaTest.ArrayExpandable`1< I4> //14代表int32 MemberRef #1 (0a00000c) ------------------------------------------------------- Member: (0a00000c) .ctor: CallCnvntn: [DEFAULT] hasThis ReturnType: Void No arguments. MemberRef #2 (0a00000d) ------------------------------------------------------- Member: (0a00000d) Add: CallCnvntn: [DEFAULT] hasThis ReturnType: Void 1 Arguments Argument #1: Var!0TypeSpec #2 (1b000002)------------------------------------------------------- TypeSpec : GenericInst Class MetaTest.ArrayExpandable`1< String> MemberRef #1 (0a00000e) ------------------------------------------------------- Member: (0a00000e) .ctor: CallCnvntn: [DEFAULT] hasThis ReturnType: Void No arguments. MemberRef #2 (0a00000f) ------------------------------------------------------- Member: (0a00000f) Add: CallCnvntn: [DEFAULT] hasThis ReturnType: Void 1 Arguments Argument #1: Var!0这时候我们就可以看出,元数据为泛型类ArrayExpandable<T>定义一份定义表,生成两份规格,也就是当你实例化类型参数为int和string的时候,分别生成了两份规格代码,同时还发现以下的现象:
var lsInt = new ArrayExpandable<int>();//引用的是类型规格1b000001的成员0a00000c .ctor构造lsInt.Add(1);//引用的是类型规格1b000001的成员0a00000d Add var lsStr = new ArrayExpandable<string>();//引用的是类型规格1b000002的成员0a00000e .ctor构造lsStr.Add("ryzen");//引用的是类型规格1b000002的成员0a00000f Addvar lsStr1 = new ArrayExpandable<string>();//和lsStr一样lsStr.Add("ryzen");//和lsStr一样非常妙的是,当你实例化两个一样的类型参数string,是共享一份类型规格的,也就是同享一份本地代码,因此上面的代码在线程堆栈和托管堆的大致是这样的:
由于泛型也有元数据的存在,因此可以对其做反射:
Console.WriteLine($"-----------{nameof(lsInt)}---------------");Console.WriteLine($"{nameof(lsInt)} is generic?:{lsInt.GetType().IsGenericType}");Console.WriteLine($"Generic type:{lsInt.GetType().GetGenericArguments()[0].Name}");Console.WriteLine("---------Menthods:");foreach (var method in lsInt.GetType().GetMethods()){ Console.WriteLine(method.Name);}Console.WriteLine("---------Properties:");foreach (var property in lsInt.GetType().GetProperties()){ Console.WriteLine($"{property.PropertyType.ToString()}:{property.Name}");}Console.WriteLine($"\n-----------{nameof(lsStr)}---------------");Console.WriteLine($"{nameof(lsStr)} is generic?:{lsStr.GetType().IsGenericType}");Console.WriteLine($"Generic type:{lsStr.GetType().GetGenericArguments()[0].Name}");Console.WriteLine("---------Menthods:");foreach (var method in lsStr.GetType().GetMethods()){ Console.WriteLine(method.Name);}Console.WriteLine("---------Properties:");foreach (var property in lsStr.GetType().GetProperties()){ Console.WriteLine($"{property.PropertyType.ToString()}:{property.Name}");}输出:
-----------lsInt---------------
lsInt is generic?:True
Generic type:Int32
---------Menthods:
get_Item
set_Item
get_Capacity
set_Capacity
get_Count
Add
GetType
ToString
Equals
GetHashCode
---------Properties:
System.Int32:Item
System.Int32:Capacity
System.Int32:Count
-----------lsStr---------------
lsStr is generic?:True
Generic type:String
---------Menthods:
get_Item
set_Item
get_Capacity
set_Capacity
get_Count
Add
GetType
ToString
Equals
GetHashCode
---------Properties:
System.String:Item
System.Int32:Capacity
System.Int32:Count
泛型编程作为.NET体系中一个很重要的编程思想,主要有以下亮点:
参考#
Design and Implementation of Generics for the .NET Common Language Runtime
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/
《CLR Via C# 第四版》
《你必须知道的.NET(第二版)》
到此这篇关于C#泛型运作原理的文章就介绍到这了,更多相关C#泛型运作原理内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!
声明:本页内容来源网络,仅供用户参考;我单位不保证亦不表示资料全面及准确无误,也不保证亦不表示这些资料为最新信息,如因任何原因,本网内容或者用户因倚赖本网内容造成任何损失或损害,我单位将不会负任何法律责任。如涉及版权问题,请提交至online#300.cn邮箱联系删除。
本文以实例形式讲述了C#泛型的用法,有助于读者深入理解C#泛型的原理,具体分析如下:首先需要明白什么时候使用泛型:当针对不同的数据类型,采用相似的逻辑算法,为了
扩展阅读c#基础系列1---深入理解值类型和引用类型c#基础系列2---深入理解String引言在上篇文章深入理解值类型和引用类型的时候,有的小伙伴就推荐说一说
一、泛型的基本概念java与c#一样,都存在泛型的概念,及类型的参数化。java中的泛型是在jdk5.0后出现的,但是java中的泛型与C#中的泛型是有本质区别
上篇文章给大家介绍了浅析C#中的类型系统(值类型和引用类型),接下来通过本文给大家介绍下c#泛型类型,说下C#中的泛型,熟练地使用泛型能提高代码的重用性,使用我
最近在看深入理解C#,发现这是一本很不错的书,将很多C#的知识点联系了起来,更像是一本C#历史书,从C#1一步步介绍到C#4。所以准备一边看,一边整理读书笔记。