c#基础学习文档

288 阅读47分钟

1、数据类型

C#的类型一般分为值类型引用类型两大类型。 值类型的实例存放在栈中,引用类型会在栈中放置一个指针指向堆中的某一块内容。

1.1、常见数据类型

关键词 简写对应的类全称(点击可以查看对应的API)值范围说明
boolSystem.Booleantrue、false该类型只有两个值,用作判断,表示“是”、“否”
sbyteSystem.SByte-128 ~ 127 (-27~27-1)该类型在内存中使用8个bit进行存储,而且带有符号。 根据最高位作符号位,所以sbyte实际表示范围为 -128~127
byteSystem.Byte0~28-18位的无符号bit
shortSystem.Int16-215~215-1short表示一个16位的短整形,其具体的值为-32768~32767
ushortSystem.UInt160~216-1ushort表示无符号16位的短整型,具体的范围为0~65535
intSystem.Int32-215~215-1int是我们常用的一个数据类型,它的数据范围为: -2,147,483,648~ 2,147,483,647 。 可以看到,是-2亿到2亿,基本满足了数据需要。
uintSystem.UInt320~232-1uint 无符号整形,最大值比 int大一倍左右,但是没有负数。 如果在计算中能保证没有负值,可以使用。 并不推荐,因为在做减法的时候,更、容易溢出
longSystem.Int64-216~216-1实际取值为-9,223,372,036,854,775,808~ 9,223,372,036,854,775,807 。 long在内存中比int占用更多字节,长度为int的两倍。 所以能表达更多的数剧。在数据库中,经常被用来做大数据表的主键。
ulongSystem.UInt640~264-164位无符号长整形,理同其他的无符号整形,在正整数中比long表达更多的数据。
charSystem.Charutf-16在.net 中char 表示 utf-16的编码单位,所以绝大多数的字符都可以用char表示,包括中文汉字。
floatSystem.Single-3.402823e38~3.402823e3832位的单精度浮点型,也就是通常说的带小数点的数
doubleSystem.Double64位双精度浮点型,比float能表示更多的小数位。 实际取值-1.79769313486232e308~1.79769313486232e308
decimalSystem.Decimal128位高精度浮点值。 常用于不能四舍五入,或者对小数点后数字要求很敏感的地方。 在128位中,一位是符号位(表示正负),96位是值本身(N),8位是比例因子(k)。 所以decimal实际值应该是 ±Nx10k,其中-28≤k≤0.其余位暂时没有使用
stringSystem.String--字符串变量,表示一组字符数组。字符串是不可变量。即字符串不能修改,任何针对字符串的修改都会生成一个新的字符串。

另外的两个特殊类型:dynamic object。 其中 dynamic 表示动态类型,这是C#在4.0开始支持的,dynamic关键字声明该变量名是个动态变量。具体使用参照 Python,Js 之类的动态语言。但是dynamic声明的变量不支持添加属性,但这并不完全绝对,可以参照后续的动态篇会对这部分内容进行介绍。

object 所有类型的父类,C#所有类都是 object的子类。不过上表中介绍的直接父类是 ValueType(表示值类型),但是ValueType的父类仍然是object。

说明: 对于 double 和 float 因为存储数据方式的问题,都存在一个问题:对于 0 或者近0的值不能很好的表达。因为浮点型变量在内存中表示为 1/2n,所以都会存在一个+0和-0两个值。当 一个浮点型判断是不是等于0时,可以正常判断。但是一旦涉及到数学运算的结果与0进行比较的话就会出现问题,比如说 0.1+0.2 != 0.3 这个判断式是 true。所以标准的判断方式应该是 Math.Abs(0.1+0.2 - 0.3)< ? 这种方式,其中?表示系统接受的误差范围。 而decimal在这方面的准确度就比 double和float高很多。至少不会出现 0.1 + 0.2 != 0.3 这种问题。所以decimal一般用在金额计算这些地方。

1.2、类型转换

类型转换是指一个类型的数值通过某种手段转换成另一种类型。 类型转换分为两种方式:默认类型转换强制类型转换

默认类型转换、强制类型转换又被称为向上转型和向下转型;

1.2.1、基本类型转换

在以下几种情况会触发基本类型转换:

  • 当一个子类想转换成它的父类时,
  • 当短精度向高精度转换时。 如: byte -> int -> long -> float -> double 参照:
  • 由低精度向高精度类型转换时默认类型转换(向上转型)
int i = 1;
double d = i;
float f = i;
d = f;
uint ui = 1;
long l = ui;
d = l;
f = l;      

这里有几个需要特别注意的地方:

  • 无符号和有符号之间的转换,如果无符号的位数与有符号之间的位数一致的话不能默认转换。
  • decimal 所有的整型均可以默认转为 decimal,即 除 doublefloat以外所有数字类型的均可以。

1.2.2、对象类型转换

在以下几种情况会触发基本类型转换(向下转型):

  • 当一个父类想转换成它的子类时,
  • 由高精度向低精度类型转换时默认类型转换(向下转型)

强制类型转换分为两种:

  1. 在变量前加目标类型强制转换,这个方法与C/C++、Java的方式一致,具体为:

    C c1 = (C)a;// 代码接上
    
  2. 使用 as 关键字,

    表示将变量 x 当做类型Y

    C c2 = a as C;// 代码接上
    

两种方式的区别:

  1. 当转型失败时会抛出错误,直接终止代码
  2. 当转型失败时将c2置为null。当前步骤不会抛出异常,如果对于空引用没有进行处理,那么会在后续中抛出空引用的异常。

案例

class A { }
class B : A{ }
class C : A{ }
​
class Test
{
    static void Main()
    {
        B b = new B();
        A a = b;// 将B的实例对象赋值给A类型变量
        B bb= b;// 将B的实例对象赋值给B类型变量
​
        Console.WriteLine("a is B ? {0}", a is B);// true
        Console.WriteLine("bb is B ? {0}", bb is B);// true
        Console.WriteLine("b is A ? {0}", b is A);// true
        Console.WriteLine("a is C ? {0}", a is C);// false
        Console.WriteLine("b is C ? {0}", b is C);// false
​
        // 强制类型转换 ,向下转型,is关键字判断类型是否一致
        A a1 = new C();
        C c1 = (C) a1;
        Console.WriteLine(c1.ToString());
        Console.WriteLine("c1 is A ? {0}", c1 is A);// true
        Console.WriteLine("c1 is B ? {0}", c1 is B);// false
​
        // 使用as关键字,as 表示将变量 x 转成类型Y
        C c2 = a1 as C;
        Console.WriteLine(c2);
        Console.WriteLine("c2 is A ? {0}", c2 is A);// true
    }
}

1.2.3、值类型的转换

在之前的内容中我们提到了小精度到大精度可以默认转换。但是我们在平常使用的过程中会遇到各种情况下的转换,那么这个时候我们就要用到强制转换了,这个时候精度丢失对于我们来说就是可以接受的。 常见的转换方式有:

  1. 使用类型强制声明转换 例:long lval = 100; int i = (int)lval;
  2. 使用 System.Convert类。使用 Convert.ToXXX(),其中XXX表示转型目标对象。
//Convert 示例代码
long lval = 19293;
var i = Convert.ToInt32(lval);
double d = 10.091;
var dc = Convert.ToDecimal(d);
var dt = Convert.ToDateTime("2019-03-30");

值得注意的是:Convert的ToXXX其中XXX使用的是C#类型名称,而不是关键字。Convert是个很有用的类,在我们开发工作中会大量的使用这个类进行值的类型转换。

2、字符串

2.1、string的特点

String(字符串)在大多数开发语言中属于不可变量,任何针对String的修改都会生成一个新的字符串。为什么会如此呢,究其原因是String在内存中以类似数组的形式存储字符数据。同时,String 还有一个非常有意思的特点: 字符串在一次声明后,程序会记忆当前已声明的字符串,以备下次使用时不用重新划分内存。 关于这一点有个很有意思(智障)的面试题:

以下三种方式一共生成了几个内存空间:

  1. string s1 = "sa1234";
  2. string s2 = "sa"+"1234";
  3. string s3 = new string(s1);

很多人包括一些老开发都会认为是4个,那么实际上呢,s2和s1同时指向了一个地址也就是两个。 嗯?是不是有点不可思议?

这个是因为在C#中,字面值有一个特点,如果声明过程中一个变量的声明使用的都是字面值(即非变量)或者是常量,那么该变量的声明过程会优先计算值,然后将该变量指向一个常量。 也就是说 在string s2 = "sa"+"1234";这个表达式中,会先将"sa"和"1234"合并成一个字符串,再赋值给s2。并不会先划分一个"sa"的内存区域,再划分一个"1234"的内存区域,最后再合并成”sa1234“。所以1和2都是直接指向"sa1234"。

2.2、string变量的声明

C#中字符串常见的声明有两种:

  1. 直接使用字面值 即String s = "12321";
  2. 使用构造器,即String s = new String("12321");

2.3、string的构造函数

string的构造函数一共有以下几种:

  1. public String(char c, int count);
  2. public String(char[] value);
  3. public String(char[] value, int startIndex, int length);
  4. 其他

第一个: 生成数量为count的由c组成的字符串,不包括其他字符。 例:string s = new String('a',10);//s="aaaaaaaaaa"

第二个: 将字符数组value转换为字符串,字符串等于字符数组依次连接生成的字符串 例:string s = new String(new char[]{'a','b','c','d','e'});//s = "abcde"

第三个: 将字符数组value中部分元素拼接为字符串 例:

var chars = new char[] {'A', 'V', 'a', 'b', 'c', 'd', 'e'};
var sr = new string(chars, 0, 5);//sr = "AVabc"

第四个:这里面包括了一些不安全代码,即用到了指针。有机会会对这部分进行介绍。

但是,到这里我们发现一个问题,new string(string str)在哪,前面不是使用了一个 new string("12321")吗? 事实是,并没有这个构造函数。那么,当我们使用new string("12321")时,实际上是哪个呢?显然可以看到它走的是 第二种系统将字符串当做字符数组传递给了string的构造函数。这也为我们揭露了string在内存中的存储方式-字符数组。由此可见字符串的不可变性其实跟数组类似。

2.4、空字符串

在string类里有一个很有意思的静态属性:string.Empty它是一个常量,表示空字符串。空字符串,顾名思义:一个空的字符串,不含任何字符,但不是NULL。

空字符串和null的区别

  • 空字符串是一个长度为0的字符串;NULL没有长度,如果强行获取.Length将抛出异常
  • 空字符串可以调用字符串所有的方法;NULL无法调用字符串对象的任何方法
  • 空字符串指向的是内存中的某一个地址,NULL则没有任何指向

判断字符串是否为空

  1. 字符串是否是空字符串:
string str = string.Empty;
bool result = str == "" || str == string.Empty || str.Length == 0;
  1. 字符串是否是空引用:
string str = null;
bool result = str == null;

除了这些繁琐的方法,c# 内部为我们增加了一个默认支持方法:

string.IsNullOrEmpty(value)

该方法验证字符串是否是NULL或者一个空串并返回验证结果 当然,在我们平时开发过程中不只是需要不是一个空串或NULL,还得是一个非空白字符的串。这时候就需要用到这个方法了:string.IsNullOrWhiteSpace(value)

2.5、字符串方法

1)StartsWith

是否是某个字符串开头或结尾

定义

public bool StartsWith(string value);
public bool EndsWith(string value);// 判断字符串是否是该value结尾

返回字符串的开头或末尾是否与value一致。

var str = "aisbjaowiue93kas";
str.StartsWith("1");// false
str.StartsWith("ai");// true

2)Contains

定义

public bool Contains(string value);// 判断字符串中是否包含该value

3)IndexOf

定义

public int IndexOf(char value);//查找字符 value 在字符串中第一次出现的位置
public int IndexOf(char value, int startIndex);//查找字符串中字符 value在 startIndex之后 第一次出现的位置
public int IndexOf(string value);//查找子串 value 在字符串中第一次出现的位置
public int IndexOf(string value, int startIndex);//查找字符串中子串value在startIndex之后第一次出现的位置

LastIndexOf与IndexOf相似方法,查找返回最后一次出现的位置,参数与上面一致。

注意,所有的下标或索引都是从0开始计算位置,如果没有找到则返回 -1。

4)Replace

替换内容

定义

public string Replace(string oldValue,string newValue);// 将oldValue替换成newValue
public string Replace(char oldValue,char newValue);

将字符串中的oldValue替换成newValue,如果字符串中不包含oldValue,则字符串不发生任何改变。

5)Trim

去除左右空格

头尾 一起操作:

public string Trim();// 去掉字符串头和尾的空格
public string Trim(char trimChar);//去除字符串头和尾的 字符 trimChar
public string Trim(params char[] trimChars);// 从字符串的头和尾中去除 trimChars包含的所有字符

只操作头:

public string TrimStart();//去掉字符串开头的空格
public string TrimStart(char trimChar);//去掉字符串开头的字符 trimChar
public string TrimStart(params char[] trimChars);// 从字符串的头去除 trimChars包含的所有字符

最后只操作尾巴:

public string TrimEnd();//去掉字符串开头的空格
public string TrimEnd(char trimChar);//去掉字符串开头的字符 trimChar
public string TrimEnd(params char[] trimChars);// 从字符串的头去除 trimChars包含的所有字符

这里有一个问题,对字符串的掐头去尾不能处理子串,都是以字符的形式进行处理的。尤其是 参数params char[] trimChars 的处理,需要格外注意。

6)大小写切换

public string ToLower();// 转换为小写字母
public string ToUpper();// 转换为大写字母

我们通常说的大小写转换是指英文字符的大小写转换,其他语言的大小写转换与对应语言设置有关。中文和数字不会发生变化,尤其是中文不会繁体化或简体化。

7)Split

分割字符串

public string[] Split(char separator, StringSplitOptions options = StringSplitOptions.None);
public string[] Split(string separator, StringSplitOptions options = StringSplitOptions.None);

以上两个方法都会将字符串分解成一个字符串数组,其中分隔符就是separator

8)Insert

插入到某个位置

public string Insert (int startIndex, string value);

在源字符串指定的下标位置插入一个字符串并返回插入的结果。

9)Remove

删除字符

//从startIndex开始,后面的所有元素都删除,包括startIndex,返回剩下的字符
public string Remove (int startIndex);
//从startIndex开始,删除 count个元素,返回剩下的字符
public string Remove (int startIndex, int count);

10)Substring

截取字符串

// 返回从startIndex开始,剩下的的元素,包括startIndex
public string Substring (int startIndex);
// 返回从startIndex开始,一共count个元素的字符串,包括startIndex
public string Substring (int startIndex, int length);

这一部分的方法比较简单,就不做示例演示了。不过需要注意的是 所有参数都应该是大于0的,且不能大于字符串的长度,包括 startIndex+count。

2.6、字符串比较

1、等值判断

因为字符串机制的问题,所以一般字符串的相等性比较分为了两种,字面相等性和引用相等性。

在C#中默认的相等性比较是字面值相等性。

1)“==”

c#中“==”判断的是字符串的字面值是否相等

string str1 = "123";
string str2 = "123";
string str3 = new string("123");
bool eq1 = str1 == str2;// true
bool eq2 = str2 == str3;// true
bool eq3 = str1 == str3;// true

2)ReferenceEquals

判断是否是同一个引用

bool req1 = object.ReferenceEquals(str1, str2);// true
bool req1 = object.ReferenceEquals(str2, str3);// false
bool req3 = object.ReferenceEquals(str1, str3);// false

在C#中,编译器会将一样字面值的字符串指向同一个地址,这个地址是在字符串的常量池中而使用new等方法创建的字符串,是在程序运行后才会确认字符串具体的值,所以这个地址是内存中再次分配的。所以上述两种判断方式会出现不同的结果。

3)Equals

是string提供的静态方法,判断的是两个字符串具体的值是否相等,与“==”比较符一样。

public static bool Equals (string a, string b); 
public static bool Equals (string a, string b, StringComparison comparisonType);

2、大小判断

对于数字、字母来说是按照ASCII码进行排序的,也就是数字、小写、大写字母这样进行排序。

当加入中文后,中文排最后,同种字符按字典顺序进行排序。

1)Compare方法

字符串排序

这个方法有一个对象的相似方法是CompareTo,这个方法是IComparable接口定义的方法,与这个方法行为一致。该方法常用的有如下几个定义:

public static int Compare (string strA, string strB);
public static int Compare (string strA, string strB, StringComparison comparisonType);

这个方法返回一个整型,大于0前者小于后者,小于0前者大于后者,值为0两者相等。

示例:

string str1 = "123a";
string str2 = "132a";
int position1 = string.Compare(str1, str2);// -1
str1 = "你好";
str2 = "我们";
int position2 = string.Compare(str1, str2);// -1

可以明显看出,排序规则基本就是字典顺序。

3、集合数组

3.1、数组

数组,集合的基础部分,主要特点是一经初始化就无法再次对数组本身进行增删元素。C#虽然添加了一些修改数组的扩展方法,但基本都会返回新的数组对象。

3.1.1 初始化

数组的初始化需要指定大小,可以显示指定或者隐式的指定。

// 显示指定类型与大小,具体的元素后续赋值
string[] strArr = new string[10]; 
//指定类型同时给元素赋值,具体大小由编译器自动推断
string[] strArr1 = new string[]{"1","2","3","4","5","6","7","8","9","10"};
// 类型和大小都由编译器进行推断
string[] strArr2 = new []{"1","2","3","4","5","6","7","8","9","10"}; 

3.1.2 常用方法

1)访问和赋值

数组可以通过下标访问数组中的元素,下标从0开始,表示0位。代码如下:

string item0 = strArr[0]; //取出 "1"
string item2 = strArr[2]; // 取出 "3"
strArr[0] = "3"; // strArr = {"3","2","3","4","5","6","7","8","9","10"}

2)获取长度

int length = strArr.Length;// 获取一个整型的长度
//获取一个长整型的长度,对于一个非常大的数组且长度可能会超过int的最大值
long longLength = strArr.LongLength;

3)循环迭代

// 普通for 循环
for(int i = 0;i < strArr.Length;i++)
{
    string it = strArr[i];
}
// foreach 循环
foreach(string it in strArr)
{
    // 依次循环,不需要下标,操作更快一点
}

4)CopyTo 复制到

public void CopyTo(Array array, int index);
public void CopyTo(Array array, long index);

参数说明: array 需要复制到的数组,index 目标数组的起始下标

方法说明:将 源数组的元素依次复制到 array从index下标开始的位置

string[] strArr1 = new string[]{"1","2","3","4","5","6","7","8","9","10"};
string[] strArr3 = new string[10];
strArr1.CopyTo(strArr3, 0); //strArr3 = {"1","2","3","4",'5","6","7","8","9","10"}

值得注意的是strArr3的长度不能 小于 index + strArr1.Length

5)Sort` 排序

这个方法不是数组对象的方法,而是 Array 提供的一个静态方法。

复制代码int[] arr1 = new[] {1, 9, 28, 5, 3, 6, 0, 12, 44, 98, 4, 2, 13, 18, 81, 92};
Array.Sort(arr1);//0,1,2,3,4,5,6,9,12,13,18,28,44,81,92,98

值得注意的是,该方法是直接对数组进行操作,所以不会返回新的数组。

6)ToList转成List`

顾名思义,将Array对象转成List对象。这里需要额外注意的是,转换成的List是不可改变长度的

Clone() 获得一个浅拷贝的数组对象,获取该对象的一个浅拷贝数组对象。

至于其他的Array类和Array对象 还有很多有意思的方法,但是平时开发的时候使用的频率比较低。

3.2、常用集合

主要集合

C#/.NET Framework 提供了很多很有意思的集合类,数组、列表、链表、Set、字典等一系列的类。其中数组是语言的一部分,个人认为严格意义上不属于集合类这一部分。C#开发中常用的集合有数组、 List类、Set接口、Dictionary类、Queue类、LinkedList类等,其他的出镜率不高。 与其他(java)语言不同的一点是,C#的List是类,而不是接口,接口是IList,但这个接口意义不大,在使用IList的时候更多的倾向于使用IEnumerable,这主要是因为IEnumerableLinq的支持再者两者的方法基本一致,能用IList的地方基本都可以用IEnumerable

3.2.1、List 列表

List列表为一个泛型类,泛型表示,其中T表示列表中存放的元素类型,T代表C#中可实例化的类型。关于泛型的具体描述以后介绍,现在回过头来继续介绍列表。列表内部持有一个数组对象,列表有两个私有变量:一个是列表容量,即内部数组的大小;另一个是存放的元素数量,通过Count获取。 List列表通过元素数量实现了AddRemove 的操作,列表对象操作引发元素数量变动时都会导致对容量的重新计算,如果现有容量不满足后续操作需要的话,将会对现有数组进行扩充。

1)初始化
List<string> list = new List<string>();// 初始化一个空的列表
List<string> list1 = new List<string>{"12", "2"};//初始化一个包含两个元素的列表
list1 = new List<string>(100);//初始化一个空的列表,并指定list的初始容量为100
list = new List<string>(list1);// 使用一个List/Array 初始化一个列表
2)常用方法
  1. CountLongCount获取元素的数量

    Count 表示获取一个int类型的的数量值,LongCount表示获取一个long类型的数量值。通常情况下两者返回的结果是一致的,但是如果列表中元素的数量超过了int允许的最大返回直接使用 Count获取将会出现数据溢出的问题,这时候就需要LongCount了。

  2. 访问元素/修改元素

    C#的列表操作单个元素很简单 ,与数组的操作方式完全一样。

    string str = list1[0];// 取 list1 的第一个元素,即下标为0的元素
    list1[2] = "233"; // 将 list1 的第三个元素设置为“233” ,即下标为2 的元素,这里假设list1有至少三个元素 
    

    需要注意的地方是,如果给定的下标超过了List对象的索引值范围会报ArgumentOutOfRangeException。判断方法就是 **下标**>=Count,如果满足就会越界。

  3. AddAddRange 添加到列表最后

    将元素添加到List的末尾,Add添加一个,AddRange添加一组,支持数组、列表。

    List<string> list = new List<string>();// 初始化一个空的列表
    list.Add("12");//list = {"12"}
    List<string> list1 = new List<string>{"14", "2"};
    list.AddRange(list1);// list = {"12","14","2"}
    
  4. Insert(int index, T item)InsertRange(int index,IEnumerable<T> items) 插入

    • Insert(int index,T item) 在 index 下标处插入一个元素,该下标以及该下标以后的元素依次后移
    • InsertRange(int index,IEnumerable<T> items) 在index下标处插入一组元素,该下标以及之后的元素依次后移

    示例:

    List<int> arr1 = new List<int>{1, 9, 28, 5, 3, 6, 0, 12, 44, 98, 4, 2, 13, 18, 81, 92};
    arr1.Insert(3,37);// arr1 = 1,9,28,37,5,3,6,0,12,44,98,4,2,13,18,81,92 下标为3的元素变成了37,之后的元素依次后移了
    List<int> arr1 = new List<int>{1, 9, 28, 5, 3, 6, 0, 12, 44, 98, 4, 2, 13, 18, 81, 92};
    List<int> arr2 = new List<int>{2,3,4,5};
    arr1.InsertRange(2,arr2);//arr1=  1,9,2,3,4,5,28,5,3,6,0,12,44,98,4,2,13,18,81,92 可以明显发现下标为2的元素发生了变化
    
  5. Contains(T item)

    是否包含 返回一个Boolean类型的结果,如果包含则返回true,如果不包含则返回false

    List<int> arr2 = new List<int>{2,3,4,5};
    arr2.Contains(8);//false
    arr2.Contains(3);//true
    
  6. Remove(T item) 删除指定元素

    List<int> arr2 = new List<int>{2,3,4,5};
    arr2.Remove(3);// arr2 = 2,4,5
    arr2.Remove(6);//arr2 = 2,4,5
    

    值得注意的是,如果删除一个不存在的元素时,不会报错,列表也不会发生任何改变。

  7. RemoveAt(int index) 删除位于下标的元素

    List<int> arr2 = new List<int>{2,3,4,5};
    arr2.RemoveAt(1);//arr2 = 2,4,5
    

    如果移除的下标超过了列表的最后一个元素的下标将会抛出异常

  8. RemoveRane(IEnumerable<T> items) 删除一组元素

    Remove(T item)一致,如果要删除的元素不在列表中,则列表元素不会发生变化。

    List<int> arr1 = new List<int>{1, 9, 28, 5, 3, 6, 0, 12, 44, 98, 4, 2, 13, 18, 81, 92};
    List<int> arr2 = new List<int>{2,3,4,5};
    arr1.RemoveRange(arr2);
    
  9. GetRange(int index,int count)

    从列表中获取一个子列表,从index开始,获取count个元素,如果源列表中从index开始剩余的元素不足count个将会报错。

  10. Clear()删除所有元素

    将列表清空,调用方法之后,列表中将不包含任何元素

  11. Reverse() 调转顺序

    将列表按照从尾到头的顺序进行排列

  12. IndexOf(T item) 查找下标

    查找元素在列表中的下标,如果没找到元素,则返回-1

  13. Sort()排序

    对列表进行排序,调用方法后,会按照默认排序方法返回一个排序结果

3.2.2、Set 集合

C#没有为Set单独设置类,一方面是因为Set出镜率不高,另一方面也因为Set本身的机制所致 Set集合不能包含重复元素,如果尝试存入重复元素集合元素将不会发生任何变化。 Set集合中元素的顺序与存放顺序不一定相同。因为Set集合中存放对于使用者而言是乱序存放的。 我们常用的Set集合有 HashSet<T>SortSet<T> ,其他的Set相关类则属于更加少见。

1)HashSet<T>SortSet<T>
  • HashSet 俗称 哈希集合或者哈希Set,内部使用Hash值作为元素的唯一性验证,即调用对象的HashCode()方法作为Hash值的来源。
  • SortSet 顾名思义,排序集合,它每次在插入的时候都会对元素进行一次排序
2)共同点

1.初始化

两者相同的地方就是 都有以下几种初始化方法

Set<T> set = new HashSet<T>();// = new SortSet<T>(); 初始化一个空的集合
//使用一个集合对象初始化
Set<T> set1 = new HashSet<T>(IEnumerable<T> items);// = new SortSet<T>(IEnumerable<T> items); 
Set<T> set2 = new HashSet<T>(){T t1, T t2, T t3};// 与上一种一样

2.添加元素

set1.Add(item);// 集合只支持添加单个元素,但是可以通过集合运算的方式增加多个元素

3.移除元素

set1.Remove(item);//删除集合中与item判断相等的元素

4.访问元素

需要注意的地方是,C#对Set没有支持下标访问方式获取Set里的元素,这是因为索引位置对于集合来说意义不大,没有操作意义。

foreach (var item in set1)
{
    // 操作
}

Set 只能通过遍历访问元素,不能通过Get或者下标操作访问元素。关于foreach循环会在下一篇《C#基础知识系列》里进行介绍。

集合运算

file

  1. UnionWith

    SortedSet<int> set = new SortedSet<int>{1,0,29,38,33,48,17};
    set.UnionWith(new []{5,57,8,4,3,1,0,33}); // set = 0,1,3,4,5,8,17,29,33,38,48,57
    

    通过传入一个集合对象,将该集合设置为两个集合的并集,也就是说取上图 A,B,C 三个区域的和

  2. ExceptWith

    SortedSet<int> set = new SortedSet<int>{1,0,29,38,33,48,17};
    set.ExceptWith(new []{5,57,8,4,3,1,0,33}); // set =17,29,38,48
    

    传入一个集合,从set中去掉同属于两个集合的元素,保留只存在于set的元素,也就是取上图中的A部分元素

  3. IntersectWith

    SortedSet<int> set = new SortedSet<int>{1,0,29,38,33,48,17};
    set.ExceptWith(new []{5,57,8,4,3,1,0,33}); // set =0,1,33
    

    传入一个集合,保留set与传入集合里相同的元素,也就是说取的是上图中的B部分

  4. SymmetricExceptWith 余集

    SortedSet<int> set = new SortedSet<int>{1,0,29,38,33,48,17};
    set.SymmetricExceptWith(new []{5,57,8,4,3,1,0,33});//set= 3,4,5,8,17,29,38,48,57
    

    传入一个集合,保留set与传入集合两个集合中不同的元素,也就是取上图的A+C这两部分。

Contains 包含

判断集合中是否包含目标元素,返回true/false

SortedSet<int> set = new SortedSet<int>{1,0,29,38,33,48,17};
set.Contains(1);// true
3)不同点

1.初始化

  • HashSet<T> 支持传入一个自定义的相等比较器,该比较器需要返回一个 bool值;可以指定起始容量
  • SortSet<T> 支持传入一个自定义的大小比较器,该比较器返回一个int值;不能指定起始容量

2.其他 Comparer 属性:SortSet 可以获取大小比较器;HashSet 获取一个相等比较器

3.2.3、Dictionary 字典

Dictionary 字典,正如它的名称一样,Dictionary 需要指定两个类型,一个作为索引键,一个作为数据值。就像字典一样,每一个词条内容都只有一个字词索引,但可以出现同义词一样。当然,作为我博大精深的中文会出现同字不同音的词组,但是一旦把音、字组合起来作为索引,那还是只会出现一个词条。 所以 Dictionary的使用方式也跟字典一样,通过索引访问和操作数据。

1)初始化

Dictionary的初始化有如下几个方法:

Dictionary<string, int> dict = new Dictionary<string, int>();// 键是字符串,值是int类型
Dictionary<string,int> dict1 = new Dictionary<string, int>(10);// 指定初始容量是10
Dictionary<string,int> dict2 = new Dictionary<string, int>()
{
    {"1",1},
    {"2",2}
};// 在大括号标记中 通过 {key,value}的写法创建一个 字典对象,并包含这些键值对// 传入一个字典对象,以传入的对象为基础创建一个字典
Dictionary<string,int> dict3 = new Dictionary<string, int>(dict2);
2)常用方法
  1. 添加元素

    Dictionary<string, int> dict = new Dictionary<string, int>();
    // 方法一
    dict.Add("1",2);//添加一个 键为“1”,值为2的键值对。
    //方法二
    //字典可以类似列表的形式通过下标添加或更新键对应的值,
    //不过与列表不同的是,字典的下标是字符串
    dict["2"] = 4;// 如果 dict中2有值,则更新为4,如果没有,则设置2对应的值为4
  2. 获取元素

    Dictionary<string, int> dict = new Dictionary<string, int>();
    /*
    省略数据填充阶段
    */
    int value = dict["2"]; // value = 4
    // 如果Dictionary中不存在索引为“2”的数据
    // 将会抛出 System.Collections.Generic.KeyNotFoundException 异常
    

    C# 的Dictionary还有一个TryGetValue方法可以用来尝试获取,他的使用方法是这样的:

    int obj = 0;
    boolean isContains = dict.TryGetValue("3", out obj);
    // 方法会返回 dict是否包含键“3”的结果,如果有 obj 则存放了dict中对应的值,如果没有,则返回false且不改变 obj 的值
    
  3. Count

    获取Dictionary里键值对的数量。

    int count = dict.Count;
    

    Dictionary没有LongCount属性,因为对于Dictionary存放数据需要比对Key的相等性,如果存放巨量数据将会对数据的访问和操作效率有影响。

  4. Keys方法

    获取Dictionary里所有的键,返回一个KeyCollection对象,不需要关心这是一个什么类型,可以简单的把它当做一个存放了键的HashSet

  5. ContainsKey()方法

    是否包含键:通常与获取元素一起使用,可以先判断Dictionary里是否有这个键,然后再进行后续操作。

  6. Remove()方法

    删除Dictionary中键对应的元素,删除后再次访问会报错。如果删除一个不存在的元素将返回flase。 操作示例:

    Dictionary<string,int> dict = new Dictionary<string, int>();
    //省略赋值操作
    bool result = dict.Remove("2");// 如果dict里包含键为“2”的元素,则result为true,否则为false
    

    另一种方法

    int value = 0;
    bool result = dict.Remove("2", out value);
    // 如果dict 里包含键为“2”的元素,则result 为 false且value为对应的值
    
  7. ContainsValue()方法

    是否包含值,与ContainsKey的用法一样,只不过遍历的是值;用处不大。

  8. Values方法

    获取值的集合类似与KeyValues

3.3、传统集合

3.3.1、ArrayList

List的非泛型版,与List操作方法一致,不过返回值是Object类型

3.3.2、SortedList

一个排序的键值对集合

using System;
using System.Collections;
public class SamplesSortedList  {
​
     public static void Main()  {
​
            // Creates and initializes a new SortedList.
            SortedList mySL = new SortedList();
             mySL.Add("Third", "!");
             mySL.Add("Second", "World");
             mySL.Add("First", "Hello");
​
            // Displays the properties and values of the SortedList.
            Console.WriteLine( "mySL" );
            Console.WriteLine( "  Count:    {0}", mySL.Count );
            Console.WriteLine( "  Capacity: {0}", mySL.Capacity );
            Console.WriteLine( "  Keys and Values:" );
            PrintKeysAndValues( mySL );
     }
​
     public static void PrintKeysAndValues( SortedList myList )  {
            Console.WriteLine( "\t-KEY-\t-VALUE-" );
            for ( int i = 0; i < myList.Count; i++ )  {
                 Console.WriteLine( "\t{0}:\t{1}", myList.GetKey(i), myList.GetByIndex(i) );
            }
            Console.WriteLine();
     }
}

3.3.3、HashTable

HashTable表示根据键的哈希代码进行组织的键/值对的集合。HashTable的结构类似于Dictionary但又与其不同,它的键值存储用的是Hash值。

虽然C#框架保留了非泛型集合元素,但不建议使用非泛型集合进行开发。

3.4、不常用集合

3.4.1、Queue<T>Queue

两个类是一对的,一个是泛型类,一个是非泛型类。该类中文名称是队列,如其名,队列讲究一个先进先出,所以队列每次取元素都是从头取,存放是放到队列尾。 操作代码如下:

3.4.2、LinkedList<T>

LinkedList,链表。与List不同的地方是,LinkedList的元素是LinkedListNode对象,该对象有四个属性,分别是List -指向列表对象,Previous指向前一个对象如果有的话,Next指向后一个对象如果有的话。所以根据元素的属性可以发现链表的工作方式,链表就像一条锁链一样,一个元素分三块,一个指向前一个元素,一个用来存放值,一个指向下一个元素,简单如下图所示:

file

所以可以明显的发现LinkedList在随机插取上比一般的要快,因为它不用维护一个数组,但是在查找和坐标操作上明显要慢很多。 LinkedList简单介绍这么多,可以看看它的一些常见操作:

  1. First 第一个元素

    获取第一个元素

  2. Last 最后一个元素

    获取最后一个元素

  3. AddAfter/AddBefore 在某个节点后/在某个节点前插入数据

  4. 支持以下参数列表:

    • (LinkedListNode node, T value)
    • (LinkedListNode node, LinkedListNode newNode)

    第一个参数表示要插入的节点位置,第二个表示要插入的节点/元素。第一个参数会校验是否属于该链表,如果不属于则会抛出一个异常。第二个可以是值,也可以是初始化好的节点对象。如果是节点对象,则判断是否归属其他链表,如果是其他链表抛出异常。

  5. AddFirst/AddLast,将元素添加到头或尾

    添加元素到头或者尾,可以使用LinkedListNode或者添加值。

  6. Remove

    删除,可以传递某个节点,或者要删除的节点里存放的值。

  7. RemoveFirst/RemoveLast 删除第一个节点,删除最后一个节点,不含参数

3.4.3、Stack<T>Stack

Stack广泛的翻译是栈,是一种后进先出的集合。在一些特殊场景里,使用十分广泛。 Stack有两个很重要的方法PopPush,出/进。Pop 获取最后一个元素,并退出栈,Push 向栈推入一个元素。 具体可以参照官方文档

3.5、其他集合类

3.5.1、System.Collections.Concurrent 线程安全

这个命名空间,提供了一系列线程安全的集合类,当出现多线程操作集合的时候,应当使用这个命名空间的集合。名称和常用的类是一一对应的,不过只提供了ConcurrentDictionary<TKey,TValue>ConcurrentQueue<T>ConcurrentStack<T>等几个集合类。具体可以查看官方文档

3.5.2、System.Collections.Immutable 不可变集合

命名空间包含用于定义不可变集合的接口和类,如果需要使用这个命名空间,则需要使用NuGet下载。

  • 共享集合,使其使用者可以确保集合永远不会发生更改。
  • 提供多线程应用程序中的隐式线程安全(无需锁来访问集合)。
  • 遵循函数编程做法。
  • 在枚举过程中修改集合,同时确保该原始集合不会更改。

4、面向对象

4.1、类和对象

4.1.1、概念

面向对象是一个抽象的概念,其本质就是对事物以抽象的方式建立对应的模型。 简单来讲,比如我有一只钢笔,那么我就可以通过分析,可以得到 这只钢笔的材第是塑料,品牌是个杂牌 ,里面装的墨是黑色的,可以用。这时候就能建立一个钢笔的模型,它在这里应该有这些属性:

file

图是一个不正确的UML类图,但是可以简单的概述一下我们抽象的结果。这个图就描述了一个我们抽象出来的钢笔类应该有哪些特性,而我手里的那只钢笔就可以看做是钢笔类的一个实例。 简单来讲,面向对象编程就是针对一个事件或者说一个过程,找到这个过程中参与的所有人、事务或者相对独立的个体,按照他们在这个过程中表现,提取出他们的特性,描述他们的行为,然后按照类别不同再抽象出类来。 所以,类是对象的抽象,对象是类的实例化

4.1.2、创建对象

public class Person
{
    private static int count;
​
    public static int Count
    {
        get { return count; }
        set { count = value; }
    }
​
    private string name;
​
    public string Name
    {
        get { return name; }
        set { name = value; }
    }
    public Person()
    {
        Name = "";
        Count = Count + 1;
    }
​
    public Person(string name)
    {
        this.Name = name;
    }
​
    public void SayHello()
    {
    }
}

其中: private string name;private static int count;这两个在C#中称为Field,也就是字段的意思; public static int Countpublic string Name 这两个在C#中称为Property,也就是属性的意思。 当然,不是说一个是private就叫字段,另一个是public就称之为属性,而是因为属性有getset来控制访问和赋值的行为

public Person()public Person(string name)是构造方法,所谓的构造方法就是初始化一个实例的方法,调用形式如下: Person p = new Person() 通过new关键字+类名+对应的参数列表即可。构造方法没有返回类型,方法名必须是类名,可以有任意个参数。

4.1.3、访问修饰符

C#常用的访问控制分为四种:

  • private限定只有同属于一个类的成员才可以访问,如果限定一个类是私有类,那么这个类必须是内部类
  • protected: 限定当前类的成员、子类可以访问, 不能用来限定外部类,同private一样,如果限定类是受保护类,这个类必须是内部类
  • internal(default):默认访问权限(同一个包下可以进行访问) ,对于类和方法来说,限定同一个DLL可以访问,其他DLL不能访问。区别是类的 internal 关键字可以省略,方法如果省略访问权限符,则默认是protected
  • public:公开,所有能引用类的地方都能访问类里的public对象,这是最开放的权限。

C#还有更多的访问控制,不过常用的只有这四种,更多的可以参照【官方文档】。

4.2、面向对象特征

概述

封装:对象的方法实现对外是隐藏的,就像我们在不拆开钢笔之前很难知道钢笔的墨水是怎么流动然后写出字的;

继承:子类天然拥有父类的属性和方法,假如我们还有一只特种钢笔,那么我们可以把这只特种钢笔抽象出的类认为是钢笔的子类,这只特种钢笔跟钢笔一样,可以用来做钢笔能做的事,虽然有时候不好用;

多态:简单来讲就是多种状态,对于面向对象来说,就是方法重写和方法重载。比如说,我们去找领导签字,领导在忙让我们把文件放那边,过一会领导派人送过来签好字的文件。如果领导有多只钢笔,那么领导用哪只笔、在什么时候、用什么姿势对于我们来说就是不确定的状态,这就是多态的一种。

4.2.1、封装

封装简单来讲就是调用方不知道被调用方的具体实现以及内部机制

  • 对外隐藏实现,防止外部篡改引发安全问题
  • 减少不必要的关联,被调用方需要调用方提供参数,但除此之外调用方只需要静待被调用方返回结果就行
  • 打包一系列的操作,防止中间发生变故

比如说一个钟表,给我们一堆零件,在没有拼接、安装好之前也就是封装好,这个钟表是不能正常使用的。只有我们按照一定逻辑,将零件安装好之后(封装),再装上电池或上发条(调用) 钟表才会转起来。 简单的用代码介绍一下:

public class Person
{
    private static int count;
​
    public static int Count
    {
        get { return count; }
        set { count = value; }
    }
​
    private string name;
​
    public string Name
    {
        get { return name; }
        set { name = value; }
    }
​
    public Person()
    {
        Name = "小明";
        Count = Count + 1;
    }
​
    public Person(string name)
    {
    }
​
    public void SayHello()
    {
        Console.WriteLine("你好,我叫"+Name);
    }
}
​
class Program
{
    static void Main(string[] args)
    {
        Person p = new Person();
        p.SayHello();
    }
}

对于Program类来讲,Person的SayHello是怎么实现的完全不知情。这就是封装的意义。

4.2.2、继承

C#的继承是单继承,也就是说一个类只有一个父类,如果不指明父类,那么它的父类就是object

换句话说,object是C#继承体系里的根,也就是所有类的祖先类。

C#的继承用: 表示,即 class B: A表示B继承A。

public class A
{
    public virtual void Say()
    {
    }
    public void Look()
    {
    }
}
public class B : A
{
    public override void Say()
    {
    }
}

简单来讲,对于A和B在Say方法有不同的实现,对于调用方来讲,它们的表现应当是一致的。换句话说,就是所有用到A的地方,都能用B来代替,这不会出现任何问题。 继承可以简化很多行为(方法)一致的写法。如示例所述,B类在Look上与其父类A类有着一致的行为和表现,那么B就可以省略了Look的定义和描述,沿用父类的方法。通过继承可以很快地建立一套丰富的方法体系。子类每一次对父类的方法补充都会在子类里体现出来。所以继承可以说是面向对象的核心体系。

有个关键字需要额外的讲解一下***saled,如果看到一个类有这个标记,那么需要注意了,这个类是不可被继承的类。***

4.2.3、多态

态的实现就是通过类的继承和方法的重载实现的。类的继承主要通过重写父类方法或者覆盖父类方法来实现的,主要关键字就是 virtualoverridenew。 具体的介绍是:

  • virtual 关键字声明函数为虚函数,意思就是子类可能会重写该方法
  • override 用在子类,用来声明该方法是重写父类的方法
  • new 跟实例化对象的new不同,这个放在方法前的意思是该方法会隐藏父类方法的实现。
public class A
{
    public virtual void Say()
    {
        //省略实现
    }
​
    public void SetName()
    {
        //省略实现
    }
}
​
public sealed class B:A
{
    public override void Say() //重写父类方法
    {
        //省略实现
    }
    public new void SetName() // 覆盖父类方法
    {
        //省略实现
    }
}
​

重写和覆盖的区别在哪呢:

编译在左,执行在右

A a = new B();
a.Say();// 调用的是 B中 Say方法
a.SetName();//调用的是A的SetName 方法
B b = (B)a;
b.SetName();//调用的是B的SetName 方法
b.Say();// 调用的是 B中 Say方法

4.3、类和接口

4.3.1、接口定义

C#中类和接口的声明方式不同,类用的关键字是class,接口用的是interface。而且类是继承,接口是实现,一个类只能有一个父类,接口可以有多个。 接口需要注意的地方就是,接口所有的方法都是public的,因为接口就是用来定义规范的,所以一旦它的方法访问控制不是public的话,就没什么意义。

public class Demo1
{
}
public interface IDemo
{
    string Method();
}
public class Demo3 : Demo1, IDemo
{
    public string Method() // 同名同参的方法为实现方法
    {
        return "test";
    }
    string IDemo.Method() // 接口名.方法名 优先实现方法
    {
        return "test2";
    }
}

接口的实现和类的继承都是 : ,先继承后实现。

观察示例代码,发现Demo3有两个方法public string Method()string IDemo.Method() 。这两个都是实现接口的方法,不同的地方是它们的使用:

IDemo idemo = new Demo3();
idemo.Method();//返回 test2
Demo3 demo = new Demo3();
demo.Method();// 返回 test

使用接口名.方法名实现方法的时候,这个方法对于实现类构造的对象来说是不可访问的。

当然两种方法可以共存,但是不会两个方法都被认为是接口的实现方法。接口优先使用接口名.方法名 作为实现方法,如果没找到则认为同名同参的方法为实现方法。

4.3.2、Object类

object 作为基类定义了四个基本方法,这四个方法是所有子类都有的方法,也是一个核心方法:

  • Equals(object obj) 这是一个很重要的方法,它是 C#中判断两个对象是否相等的依据,也就是 == 运算符的结果,如果不重写这个方法的话,返回的结果是两个对象是否指向同一个引用地址。
  • GetType() 返回这个对象的类型,这是反射机制中重要的一块
  • ToString() 返回字符串,获得一个对象的文字描述,默认返回的是对象的地址描述信息,这个方法建议重写
  • GetHashCode() 返回 Hash值,某些集合和程序机制会以HashCode作为元素的相等性判断依据,所以在重写 Equals 之后也要重写 这个方法,并保证两个方法对于相同的对象做相等性结果判定是应该表现一致。

4.4、扩展方法

C# 有一个很重要的机制就是扩展方法,扩展方法表现出的跟类自有的方法调用结果一致。 具体写法如下:

public static class Methods
{
    public static string Test(this Person person)
    {
            return "test";
    }
}

需要注意的是,扩展方法所在类必须是静态类,扩展方法必须是静态方法,扩展方法第一个参数就是扩展的元素对象,用this标记。

不过很多人对扩展方法褒贬不一,有人认为扩展方法极易破坏继承链,导致一些不必要的麻烦;有人认为扩展方法就跟工具方法一样,而且可以优化调用方式,统一使用风格。

不过我看来,扩展方法利大于弊。因为扩展方法可以在不修改原有类的基础上增加功能,同时它也是一个工具类,跟普通的方法是一致的。

5、反射和泛型

5.1、泛型

5.1.1、概念

泛型说的笼统一些就是类型参数化的过程,我们之前介绍的List就是一个泛型类。泛型分泛型类/接口和泛型方法。泛型类和泛型接口可以看做是一种,因为它的泛型参数是用在整个结构体里面的(注意不是结构,struct);泛型方法又有参数泛型和返回值泛型两种。

5.1.2、泛型定义

public class Template<T>
{
    private T data;
    public void SetTemplate(T temp)
    {
        data = temp;
    }
    public T GetTemplate()
    {
        return data;
    }
}

在声明类的时候,声明一个泛型占位符T ,在下面的属性、字段、方法的参数和方法的返回值都可以使用这个占位符,约定类型一致。

泛型的接口和泛型类是一致的,只不过接口没有方法的实现内容也就是方法体而已。

使用

// 继续上面的代码
Template<int> temp = new Template<int>();
temp.SetTemplate(10);
int ten = temp.GetTemplate();

使用泛型类和普通类不同的地方就是,泛型类告诉编译器你要传递的类型。使用<> 做标记,中间写类型,表示这是一个泛型为XXX的泛型类。 通常与其他语言不同的地方是,C#的泛型支持所有类型,意思就是在没有额外声明的时候,可以使用任意类型作为泛型参数传递。

5.1.3、泛型方法

C#也可以声明一个方法为泛型方法,方法的泛型声明是声明在方法名的后面,参数列表的前方

public void TemplateMethod<T>(T arg);
public T TemplateMethod1<T>();
public T TemplateMethod2<T>(T arg);

上述三个都是合规的泛型方法声明。泛型可以是参数,也可以是返回值,还能既是返回值又是参数。

那么问题来了,多个泛型参数该怎么声明?

如下:

public T2 TemplateMothod3<T1,T2>(T1 arg);
public T3 TemplateMothod4<T1,T2,T3>(T1 arg,T2 arg2);

在两个尖括号中间放入多个泛型,然后用逗号隔开,与参数列表和返回值的类型一一对应。

方法的使用

TemplateMethod(10);// 方式 1
int it = TemplateMethod1<int>();// 方式 2

这里简单介绍一下泛型方法的使用:

  • 方式1 隐藏了一个泛型参数,这是因为如果泛型是参数的话,c#会根据参数的类型自动解析对应的泛型类型是什么,方式1 等同于TemplateMethod<int>(10);
  • 方式2 当泛型参数是返回值时,必须告知具体的泛型类型。

5.1.4、泛型约束

在实际开发过程中,我们会对一些泛型类的泛型参数进行类型约束,那么泛型约束应该怎么写呢,看示例:

public void Demo<T>(T arg) where T : 约束内容
public void Demo<T,P>(T arg,P arg1) where T: 约束内容 where P:约束内容

如果对多个参数进行约束,就写多个where。

泛型的约束有一下几种:

  • class 表示这是个引用类型
  • new() 表示必须有一个无参构造函数
  • struct 表示是个结构体
  • 具体的类名或接口名 表示这个参数必须是这个类的子类或接口的实现类

5.1.5、泛型标记

在C#里有个很有意思的地方,那就是泛型标记。

泛型支持 in/out作为占位符T的前置标记。那这两个标记是什么意义呢,in表示这个类型参数只能作为参数列表的类型进行传递,out表示这是一个返回值的类型,示例如下:

public T2 Demo<in T1,out T2>(T1 t1);

类和方法的标记大同小易,基本上是一致的。

5.2、反射

5.2.1、类型对象

1)概念

需要注意的一个类:Type,这个类是反射技术里的基石,甚至可以说是核心,表示一个类的类型信息。

那么,我们该如何获取类型对象呢?在C#中s常见的有如下两个方法:

1.使用typeof 关键字

Type personType = typeof(Person);

2.通过对象,使用GetType 方法

Person person = new Person();
Type personType = person.GetType();

如果我们在编写程序的时候,知道要获取什么类的Type对象的话,建议使用typeof获取。如果我们只有一个对象,需要通过这个对象进行操作的话,那么最好使用GetType来获取。

2)作用

  1. 获取类名personType.Name

    获取到的结果是Person 这个值。

  2. 获取所有属性personType.GetProperties()

    该方法会返回一个类型为PropertyInfo[] 的数组,这个数组里包含着所有使用public声明的属性。当然也可以通过指定的属性名获取属性对象:personType.GetProperty("Name") 这里会获取到Person类的Name属性。

  3. 获取所有方法personType.GetMethods()

    获取该类所有public的方法,并将其封装成一组类型是MethodInfo的对象数组。同理,也可以根据方法名进行检索:personType.GetMethod("SayHi") ,就能获取对应的SayHi方法。不过,如果有同名方法的话,就可能会出现获取到的方法不是你想要的了。

  4. 获取所有构造函数personType.GetConstructors()

    获取构造函数,返回的是一个类型是ConstructorInfo的数组,表示所有的构造方法,不过可惜的是,没有根据名字检索的方法了,因为构造方法就一个名。

5.2.2、PropertyInfo对象

对对象的属性进行操作

Person person = new Person();
Type personType = person.GetType();
PropertyInfo prop = personType.GetProperty("Name");//获取Name属性
Object value = prop.GetValue(person);// 获取 对象 person 的Name属性值
prop.SetValue(person, "wangyipeng");// 为对象 person的Name属性设置值为 wangyipeng

注意,如果 类的属性只有get,那么在调用SetValue时会报错。可能要问了,我们知道是有set,但是程序怎么判断呢?通过prop.CanWrite 的值进行判断,如果值是true则表明这个属性可以写入值,否则不能。

同理,可以很轻易的联想到如果只有set,那么GetValue也会报错,与之相对应的就是prop.CanRead属性了。

5.2.3、MethodInfo对象

手动执行一个方法

Person person = new Person();
Type personType = person.GetType();
MethodInfo method = personType.GetMethod("SayHi");

MethodInfo有一个Invoke方法,这个方法有两个重载版本。其中有一个是:Invoke(object obj, object[] parameters),第一个参数是要执行的方法所属的对象,后面的数组参数是对应方法的参数列表,如果为空则填null即可。该方法有个返回值,类型是object,如果方法是没有返回值的方法,那么Invoke的返回值就是null。

5.2.4、反射获取对象

通过反射获取一个类的类型对象有几种方式,先介绍一个不用类型的方式:

执行类对象的默认的构造方法创建对象

Person p = Activator.CreateInstance<Person>();

这种方式有一个要求,Person必须有一个无参的构造函数。

第二种方式:

执行类对象的默认的构造方法创建对象

Type personType = typeof(Person);
object p = Activator.CreateInstance(personType);//使用无参构造函数
p = Activator.CreateInstance(personType, "小王", 19);//使用Person(string,int)这个构造函数

当需要传递参数的时候,参数类型必须与对应的构造函数一一对应,如果顺序变了,可能会出现找不到对应类的问题。

第三种:

通过获取构造器执行构造器方法进行创建对象

//types 是参数列表的参数类型集合,顺序与实际参数顺序一致
ConstructorInfo cons = personType.GetConstructor(Type[] types);
/*
实际上应该是这个调用方
ConstructorInfo cos = personType.GetConstructor(new[]{ typeof(string), typeof(int)});
*/
object person  = cos.Invoke(new object[] {"王先生", 19});

5.2.5、反射获取泛型

可以通过反射获取到对象和方法中的实际泛型类型。

var stuType = typeof(Student);
var genericTypes = stuType.GenericTypeArguments;// 获取类的泛型参数
​
var method = stuType.GetMethods()[0];
var types = method.GetGenericArguments();// 获取方法的泛型参数