常量

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ConstTest
{
public const double PI = 3.14;
public const int a = 0;
}

class ConstTestMain
{
public static void Main(string[] args)
{
Console.WriteLine(ConstTest.PI);
}
}
  • const只能用来修饰基本数据类型、字符串类型、同为const的变量。
  • const修饰的变量不能再用static变量进行修饰,并且其只能在字段的声明中初始化, 所以const必须初始化不允许动态修改,const的原理是在编译期直接对变量值进行替换的。所以虽然他没有被static变量修饰,但他还是一个静态变量可以被类名.直接调用
  • readonly修饰符,初始化时机:运行时,可以声明时赋值或在类中的构造函数中赋值,只允许在构造函数中修改(主要作用于实例化的对象的只读属性)
1
final int a = 1;

可空类型(Nullable)

可空类型可以表示其基础值类型正常范围内的值,再加上一个 null 值

1
2
int a;	//默认值为0
int? b; //默认值为null

**Null合并运算符(??)**为类型转换定义了一个预设值,以防可空类型的值为 Null

1
2
double? num1 = null;
double num3 = num1 ?? 5.34; // num1 如果为空值则返回 5.34

二维数组

1
2
3
4
5
6
7
8
9
10
11
12
13
int[,] arr2 = new int[3, 3];
int[,] arr3 = new int[3, 3] { { 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 } };
int[,] arr4 = new int[,] { { 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 } };


//交错数组
int[][] arr5 = new int[][] { { 1, 2, 3 },
{ 4, 5 },
{ 7} };
1
2
3
4
5
6
7
int[][] arr2 = new int[3][3];
int[][] arr3 = new int[3][3] { { 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 } };
int[][] arr4 = new int[][] { { 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 } };

变长参数和参数默认值

1
2
3
4
5
6
7
8
9
10
11
12
13
//变长参数关键字 params
static int Sum(params int[] arr)
{
int sum = 0;
for (int i = 0; i < arr.Length; i++)
{
sum += arr[i];
}
return sum;
}

//调用
Sum(1,2,3,4,5,6,7,8,1,22,456,123,1);
1
2
3
4
5
6
7
8
//有参数默认值的参数 一般称为可选参数
static void Speak(string str = "我没什么话可说")
{
Console.WriteLine(str);
}
//调用
Speak();
Speak("123123");
1
2
3
4
5
public static void test(Object...objects){
for (Object object : objects) {
System.out.println(object);
}
}

结构体

在 C# 中,结构体是值类型数据结构。它使得一个单一变量可以存储各种数据类型的相关数据,它是数据和函数的集合

1
2
3
4
5
6
7
8
9
10
11
struct 自定义结构体名
{
// 第一部分
// 变量

// 第二部分
// 构造函数(可选)

// 第三部分
// 函数
}

注:结构体不允许自己申明无参构造,如果声明了构造函数,那么必须在其中对所有变量数据初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using System;
using System.Text;
     
struct Books
{
   public string title;
   public string author;
   public string subject;
   public int book_id;
};  

public class testStructure
{
   public static void Main(string[] args)
   {

      Books Book1;        /* 声明 Book1,类型为 Books */
      Books Book2 = new Books();        /* 声明 Book2,类型为 Books */

      /* book 1 详述 */
      Book1.title = "C Programming";
      Book1.author = "Nuha Ali";
      Book1.subject = "C Programming Tutorial";
      Book1.book_id = 6495407;

      /* book 2 详述 */
      Book2.title = "Telecom Billing";
      Book2.author = "Zara Ali";
      Book2.subject =  "Telecom Billing Tutorial";
      Book2.book_id = 6495700;

      /* 打印 Book1 信息 */
      Console.WriteLine( "Book 1 title : {0}", Book1.title);
      Console.WriteLine("Book 1 author : {0}", Book1.author);
      Console.WriteLine("Book 1 subject : {0}", Book1.subject);
      Console.WriteLine("Book 1 book_id :{0}", Book1.book_id);

      /* 打印 Book2 信息 */
      Console.WriteLine("Book 2 title : {0}", Book2.title);
      Console.WriteLine("Book 2 author : {0}", Book2.author);
      Console.WriteLine("Book 2 subject : {0}", Book2.subject);
      Console.WriteLine("Book 2 book_id : {0}", Book2.book_id);      

      Console.ReadKey();

   }
}

结果:

1
2
3
4
5
6
7
8
Book 1 title : C Programming
Book 1 author : Nuha Ali
Book 1 subject : C Programming Tutorial
Book 1 book_id : 6495407
Book 2 title : Telecom Billing
Book 2 author : Zara Ali
Book 2 subject : Telecom Billing Tutorial
Book 2 book_id : 6495700
  • 结构可带有方法、字段、索引、属性、运算符方法和事件。
  • 结构可定义构造函数,但不能定义析构函数。但是,您不能为结构体定义无参构造函数。无参构造函数(默认)是自动定义的,且不能被改变。
  • 与类不同,结构不能继承其他的结构或类。
  • 结构不能作为其他结构或类的基础结构。
  • 结构可实现一个或多个接口。
  • 结构成员不能指定为 abstract、virtual 或 protected。
  • 当您使用 New 操作符创建一个结构对象时,会调用适当的构造函数来创建结构。与类不同,结构可以不使用 New 操作符即可被实例化。
  • 如果不使用 New 操作符,只有在所有的字段都被初始化之后,字段才被赋值,对象才被使用。

1、访问修饰符

  • public:同一程序集中或引用该程序集的其他程序集都可以访问该类型或成员
  • Internal同一程序集中的任何代码都可以访问该类型或成员。
  • protected:访问仅限于包含类和派生类(派生类可以是不同程序集
  • protected internal:访问仅限于当前程序集或从包含类派生的类型(相当于internal和protected两个权限相加)
  • private:访问仅限于类内
  • private protected:访问仅限于包含类和派生类(派生类只能是统一程序集

2、默认修饰符

  • C#类的默认访问标识符是 internal,成员的默认访问标识符是 private
  • Java类的默认访问标识符是 default,成员的默认访问标识符是 default

3、析构函数

  • 析构函数用于在结束程序(比如关闭文件、释放内存等)、垃圾回收之前释放资源。析构函数不能继承或重载。
  • 析构函数的名称是在类的名称前加上一个波浪形(~)作为前缀,它不返回值,也不带任何参数。

4、静态类

用static修饰的类,特点:只能包含静态成员,不能被实例化

重载

在同一语句块(class或者struct)中,函数(方法)名相同或者参数的数量相同,但参数的类型或顺序不同

ref,out可以和其他参数作重载,但是两者不可以互相重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static float CalcSum(float f, int a)
{
return f + a;
}

//ref 和 out

// ref和out 可以理解成 他们也是一种变量类型 所以可以用在重载中 但是 ref和out不能同时修饰
static float CalcSum(ref float f, int a)
{
return f + a;
}

//ref和out不能互相作重载条件,但是可以和其他类型
//错误 上面有一个static float CalcSum(ref float f, int a)方法了
//static float CalcSum(out float f, int a)
//{
// f = 10;
// return f + a;
//}

重写

C#中的重写, 子类的重写方法需要加上override关键字, 父类被重写的方法则如果是普通方法则需要在virtual关键字, 如果是抽象方法则加abstract

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
namespace Test
{
public class ShapeA
{
// 虚方法
public virtual void Draw()
{
Console.WriteLine("执行基类的画图任务");
}
}
class Circle :ShapeA
{
public override void Draw()
{
Console.WriteLine("画一个圆形");
}
}

abstract class ShapeB
{
abstract public void Draw();
}
class Rectangle: ShapeB
{
public override void Draw()
{
Console.WriteLine("画一个长方形");
}
}

class Program
{
static void Main(String[] args)
{
ShapeA circle = new Circle();
ShapeB rect = new Rectangle();
circle.Draw();
rect.Draw();
}
}
}

属性

属性(Property)是域(Field)的扩展,且可使用相同的语法来访问。它们使用 访问器(accessors) 让私有域的值可被读写或操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
using System;
namespace runoob
{
   class Student
   {

      private string code = "N.A";
      private string name = "not known";
      private int age = 0;

      // 声明类型为 string 的 Code 属性
      public string Code
      {
         get
         {
            return code;
         }
         set
         {
            code = value;
         }
      }
   
      // 声明类型为 string 的 Name 属性
      public string Name
      {
         get
         {
            return name;
         }
         set
         {
            name = value;
         }
      }

      // 声明类型为 int 的 Age 属性
      public int Age
      {
         get
         {
            return age;
         }
         set
         {
            age = value;
         }
      }
      public override string ToString()
      {
         return "Code = " + Code +", Name = " + Name + ", Age = " + Age;
      }
    }
    class ExampleDemo
    {
      public static void Main()
      {
         // 创建一个新的 Student 对象
         Student s = new Student();
           
         // 设置 student 的 code、name 和 age
         s.Code = "001";
         s.Name = "Zara";
         s.Age = 9;
         Console.WriteLine("Student Info: {0}", s);
         // 增加年龄
         s.Age += 1;
         Console.WriteLine("Student Info: {0}", s);
         Console.ReadKey();
       }
   }
}

运算符重载

让自定义类和结构体对象可以进行运算

1
public static 返回类型 operator 运算符(参数列表){...}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static Point operator +(Point p1, int value)
{
Point p = new Point();
p.x = p1.x + value;
p.y = p1.y + value;
return p;
}

public static Point operator +(int value, Point p1)
{
Point p = new Point();
p.x = p1.x + value;
p.y = p1.y + value;
return p;
}

当重载二元运算符时,参数之一必须包含本身类型,并且根据参数顺序决定左操作数和右操作数。

密封类

sealed密封关键字修饰的类,让类无法再被继承

1
2
3
sealed class Son{

}

显示实现接口

当一个接口中一个方法是protected时,继承的类需要显式实现接口;

当一个类继承两个接口,但是接口中存在着同名方法时,需要显式实现接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
interface IAtk
{
void Atk();
}

interface ISuperAtk
{
void Atk();
}

class Player : IAtk, ISuperAtk
{
//显示实现接口 就是用 接口名.行为名 去实现
void IAtk.Atk()
{

}

void ISuperAtk.Atk()
{

}

public void Atk()
{

}
}

索引器

索引器(Indexer) 允许一个对象可以像数组一样使用下标的方式来访问。语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//访问修饰符 返回值 this[参数类型 参数名, 参数类型 参数名.....]{...}
element-type this[int index]
{
   // get 访问器
   get
   {
      // 返回 index 指定的值
   }

   // set 访问器
   set
   {
      // 设置 index 指定的值
   }
}

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
using System;
namespace IndexerApplication
{
   class IndexedNames
   {
      private string[] namelist = new string[size];
      static public int size = 10;
      public IndexedNames()
      {
         for (int i = 0; i < size; i++)
         namelist[i] = "N. A.";
      }
      public string this[int index]
      {
         get
         {
            string tmp;
            if( index >= 0 && index <= size-1 )
            {
               tmp = namelist[index];
            }
            else
            {
               tmp = "";
            }
            return ( tmp );
         }
         set
         {
            if( index >= 0 && index <= size-1 )
            {
               namelist[index] = value;
            }
         }
      }

      static void Main(string[] args)
      {
         IndexedNames names = new IndexedNames();
         names[0] = "Zara";
         names[1] = "Riz";
         names[2] = "Nuha";
         names[3] = "Asif";
         names[4] = "Davinder";
         names[5] = "Sunil";
         names[6] = "Rubic";
         for ( int i = 0; i < IndexedNames.size; i++ )
         {
            Console.WriteLine(names[i]);
         }
         Console.ReadKey();
      }
   }
}

当然,索引器(Indexer)可被重载。索引器声明的时候也可带有多个参数,且每个参数可以是不同的类型。没有必要让索引器必须是整型的。C# 允许索引器可以是其他类型,例如,字符串类型。

泛型

6种泛型约束

约束 关键字
值类型 where 泛型字母:struct
引用类型 where 泛型字母:class
存在无参公共构造函数 where 泛型字母:new()
某个类本身或者其派生类 where 泛型字母:类名
某个接口的派生类型 where 泛型字母:接口名
另一个泛型类型本身或者派生类型 where 泛型字母:另一个泛型字母

泛型约束组合使用:

1
2
3
4
class Test7<T> where T: class,new()
{

}

多个泛型有约束:

1
2
3
4
class Test8<T,K> where T:class,new() where K:struct
{

}

特性

C#的特性类似与Java的注解,它分为*两种类型的特性:预定义特性和自定义特性。*其中预定义特性:

  • AttributeUsage
  • Conditional 修饰条件方法,按条件编译
  • Obsolete 标记过时

AttributeUsage类似于Java的元注解, 语法:

1
2
3
4
5
[AttributeUsage(
validon,
AllowMultiple=allowmultiple,
Inherited=inherited
)]

使用:

1
2
3
4
5
6
[AttributeUsage(AttributeTargets.Class |
AttributeTargets.Constructor |
AttributeTargets.Field |
AttributeTargets.Method |
AttributeTargets.Property,
AllowMultiple = true)]

自定义特性(派生自 System.Attribute 类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 一个自定义特性 BugFix 被赋给类及其成员
[AttributeUsage(AttributeTargets.Class |
AttributeTargets.Constructor |
AttributeTargets.Field |
AttributeTargets.Method |
AttributeTargets.Property,
AllowMultiple = true)]

public class DeBugInfo : System.Attribute
{
  private int bugNo;
  private string developer;
  private string lastReview;
  public string message;

  public DeBugInfo(int bg, string dev, string d)
  {
      this.bugNo = bg;
      this.developer = dev;
      this.lastReview = d;
  }

  public int BugNo
  {
      get
      {
          return bugNo;
      }
  }
  public string Developer
  {
      get
      {
          return developer;
      }
  }
  public string LastReview
  {
      get
      {
          return lastReview;
      }
  }
  public string Message
  {
      get
      {
          return message;
      }
      set
      {
          message = value;
      }
  }
}

反射

1
2
System.Reflection.MemberInfo info = typeof(MyClass);
Type type = typeof(Rectangle);

System.Reflection 类的 MemberInfo 对象需要被初始化,用于发现与类相关的特性(attribute)

委托

委托是函数的容器,可以理解为表示函数的变量类型,本质是一个类,所以可以声明在类外,与类平级,用来定义函数的类型(返回值和参数类型),装载的函数的声明格式必须和委托的声明格式相同(返回值和参数类型),用来 存储、传递函数(方法)

委托声明语法 :函数声明语法前面加一个delegate关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//声明了一个用来装载 返回值为void无参的函数 的委托
delegate void MyFun();

void Fun(){
Console.WriteLine("fun");
}
MyFun f = new MyFun(Fun); //实例化委托变量f 装载函数Fun
f.Invoke(); //调用委托的函数

//声明了一个用来装载 返回值为int 参数为int的函数 的委托
delegate T MyFun2(T i); //委托可以用泛型
int Fun2(int i){
Console.WriteLine(i);
}
MyFun2 f2 = Fun2; //装载方式二
f2(1); //调用方式二

委托常用在:

  1. 作为类的成员
  2. 作为函数的参数

多播委托(存储多个函数, 多播委托只能得到封装的最后一个方法的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//声明了一个用来装载 返回值为int无参的函数 的委托
delegate void MyFun();

void Fun(){
Console.WriteLine("fun");
}
MyFun f = new MyFun(Fun); //装载函数Fun
f += Fun;

f(); //Fun函数被调用两次,因为委托加了两个Fun函数

f -= Fun; //移除指定函数
f(); //调用一次Fun函数

f = null //委托清空

+= 的方式为赋值后的委托变量添加多个方法(实例化算赋值,将委托变量=null也算赋值

所以一般这样使用:

1
2
3
4
5
6
7
//类中声明
public delegate void MyDelegate();
private MyDelegate1 myDelegate;

//其他地方使用
myDelegate += Func1;
myDelegate += Func2;

第一次给委托变量赋值要用 “=”,这里看上去没有给委托赋值,实际上类中的成员变量会被默认初始化,执行了=null赋值

系统自带的委托

  • Action: 无参 无返回值的函数委托
  • Action<T…>: 多个参数(最多16个) ,无返回值的函数委托
  • Func : 无参,返回值为 T 的泛型函数的委托
  • Func<T…, M> : 多个参数(最多16个),返回值为 M 的泛型函数的委托

Unity自带委托

引入命名空间:using UnityEngine.Events;
定义方式类似:public UnityAction action;
和 C# 的 Action 一样,UnityAction 可以引用带有一个 void 返回类型的方法,但是它最多只有4个参数的重载

委托原理

委托反编译结果

委托变量的调用本质上通过反射对方法的调用,委托类同时提供了两个只读属性Target和Method供使用,是将委托变量指向的对象和方法进行了包装,如果方法是静态方法,则Target为null,否则就指向对象的引用。Method属性返回一个System.Reflection.MethodInfo对象的引用。在我们调用Invoke方法时,其实是执行了委托变量指向的方法,只不过有一个内部包装和调用的机制。

​ 多播委托执行+=操作,反编译结果:

1
2
3
4
5
6
private delegate string AddDelegate<T>(T i, T j);
//...

AddDelegate<int> add = MyAdd;
add += MyAdd;
add += MyAdd;
1
2
add = (AddDelegate<int>)Delegate.Combine(add, new AddDelegate<int>(this.MyAdd));
add = (AddDelegate<int>)Delegate.Combine(add, new AddDelegate<int>(this.MyAdd));

调用了基类Delegate的Combine方法加入了方法链。

异步调用

1.异步调用的本质和适合场景
这是异步调用方式,本质上是建立了一个线程,是简化的线程调用方法。

比较适合在后台运行比较耗费时间的简单任务,要求任务之间是独立的、建议任务中不有要直接访问可视化控件的逻辑
如果后台任务必须按照特定顺序执行,或者需要访问共享资源,异步编程不太适合,直接用多线程开发技术。

异步调用

2.BeginInvoke跟Invoke的区别
调用Invoke,在Invoke的方法返回前,这个线程会阻塞;调用BeginInvoke,在BeginInvoke的方法返回前,这个线程不会阻塞!

**引用:**https://blog.csdn.net/shuaihj/article/details/53055788

事件

事件是基于委托的存在,他让委托的使用更具有安全性

它在C#语言定义文档中描述为 是一种类型成员作为成员变量 存在于类、接口和结构体

它与委托的区别:(委托是一种类,而事件是类里面的一个成员。)

  • 不能在类外部 赋值(可以 ± 函数,但是不能直接赋值,可以避免 = null将函数清空)
  • 不能在类外部 调用

声明语法:

1
访问修饰符 event 委托类型 事件名;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Test{
//委托声明
public Action myFun;
//事件声明
public event Action myEvent;

public Test(){
//委托的操作
myFun = TestFun;
myFun += TestFun;
myFun -= TestFun;
myFun();
myFun.Invoke();
myFun = null;

//事件的操作
myEvent = TestFun;
myEvent += TestFun;
myEvent -= TestFun;
myEvent();
myEvent.Invoke();
myEvent = null;
}


public void TestFun(){
Console.WriteLine("fun");
}
}

事件的声明和使用2:

1.先声明委托:

1
public delegate void OnOrderEventHandler(float price);

​ 注:如果一个委托是为事件准备的,那么它有一个C#命名规范 ,在事件委托名后加上 EventHandler 。正如刚刚声明的,事件名是 OnOrder,那么为该事件准备的委托就命名为 OnOrderEventHandler

2.然后先声明委托类型的字段,再声明事件,声明事件需使用 event 关键字,事件完整声明格式–不常见:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//声明一个委托字段
private OnOrderEventHandler onOrderEventHandler;
//声明事件的完整格式。可以看出事件是委托字段的一个包装器,正如属性是字段的包装器一样
//因为要让其他类能够访问这个私有委托,所以事件声明为public
public event OnOrderEventHandler OnOrder
{
add //添加事件处理器
{
onOrderEventHandler += value; //value是关键字,指代之后传进来的事件处理器
}
remove //移除事件处理器
{
onOrderEventHandler -= value;
}
}

事件的声明是为委托字段提供 add 和 remove 构造器,上面的这些代码都可以浓缩成以下这句(事件简略声明格式):

1
public event OnOrderEventHandler OnOrder;

C#提供了一个通用的专门给事件使用的委托声明:

1
2
3
namespace System{
public delegate void EventHandler(object sender, EventArgs e);
}

从事件完整声明格式可知,事件它不是一个字段,更不是一个特殊委托类型的字段,他是一个委托类型字段的包装器

Unity提供的事件UnityEvent<T>,可以序列化,unity编辑器上可以显示出来并且可以拖拽选择函数。而C#的事件不能序列化,重启游戏后函数清空

协变逆变

  • 协变:out
  • 逆变:in

用于在泛型中 修饰 泛型字母的;只有泛型接口和泛型委托能使用

用out修饰的泛型 只能作为返回值

1
delegate T TestOut<out T>();

用in修饰的泛型 只能作为参数

1
delegate void TestIn<in T>(T t);

协变 out:父类总是能被子类替换,允许子类返回值委托赋值给父类返回值委托。

1
2
3
4
5
6
7
8
9
10
11
//这里声明了一个Son委托变量os,委托函数返回Son对象
TestOut<Son> os = () =>
{
return new Son();
};
//然后声明了一个Father委托变量of = os;
//由于是out协变,允许子类委托赋值给父类委托
//因为里氏替换原则,父类可以装子类。
TestOut<Father> of = os;
//实际上 返回的 是os里面装的函数 返回的是Son
Father f = of();

逆变 in:父类总是能被子类替换,允许父类参数委托赋值给子类参数委托

1
2
3
4
5
6
7
8
9
10
//这里声明了一个Father委托变量if,参数类型为Father
TestIn<Father> iF = (value) =>
{

};
//然后声明了一个Son委托变量is,参数类型为Son
//由于是in逆变,允许父类委托赋值给子类委托
//因为里氏替换原则,父类可以装子类
TestIn<Son> iS = iF;
iS(new Son());//实际上 调用的是 iF

总结:

协变和逆变的主要区别在于类型转换的方向。在协变中,子类型转换成父类型,而在逆变中,父类型转换成子类型。

​ 逆变应用于参数类型。例如,假设有一个接口Comparator,其中定义了一个方法compare(T t1, T t2),而Dog和Cat都实现了该接口。如果要在Dog和Cat之间进行比较,就需要实现比较器DogComparator和CatComparator。在这种情况下,DogComparator应该能够接受任何Animal类型的参数,而CatComparator同样也应该能够接受任何Animal类型的参数。因此,这就是逆变的应用。

extern关键字

extern 修饰符用于声明在外部实现的方法。 extern 修饰符的常见用法是在使用 Interop 服务调入非托管代码时与 DllImport 特性一起使用。 在这种情况下,还必须将方法声明为 static,如下面的示例所示:

1
2
[DllImport("avifil32.dll")]
private static extern void AVIFileInit();

示例https://blog.csdn.net/weixin_43945471/article/details/112473159)

1.创建以下 C 文件并将其命名为 cmdll.c:

1
2
3
4
5
6
// cmdll.c
// Compile with: -LD
int __declspec(dllexport) SampleMethod(int i)
{
return i*10;
}

2.从 Visual Studio 安装目录打开 Visual Studio x64(或 x32)本机工具命令提示符窗口,并通过在命令提示符处键入“cl -LD cmdll.c”来编译 cmdll.c 文件。

3.在相同的目录中,创建以下 C# 文件并将其命名为 cm.cs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// cm.cs
using System;
using System.Runtime.InteropServices;
public class MainClass
{
[DllImport("Cmdll.dll")]
public static extern int SampleMethod(int x);

static void Main()
{
Console.WriteLine("SampleMethod() returns {0}.", SampleMethod(5));
}

}

从 Visual Studio 安装目录打开一个 Visual Studio x64(或 x32)本机工具命令提示符窗口,并通过键入以下内容来编译 cm.cs 文件

this关键字–拓展类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class User
{
public string userName;
}
public static class UserEx
{
public static void say(this User user){
Console.WriteLine(string.Format("嗨,大家好!大家可以叫我{0}!", user.userName));
}
}

class Program
{
static void Main(string[] args)
{
User user = new User();
user.userName = "userName";
user.say();
}
}
静态类静态方法 `say`,会拓展为`User`类的一个**实例方法**

注意:

  • 自身方法优先于拓展方法的调用。
  • 不能为静态类扩展方法

ArrayList

本质上是一个自动扩容object数组(默认大小4,扩容为原来2倍大小),所以存储值类型的数据时会有装箱拆箱发生

List

本质是一个可变类型的泛型数组, 其他和ArrayList一样

HashTable

底层也是一个自动扩容的object数组(默认大小3,扩容为两倍于原先容量最小的素数:3->7)

Hashtable 采用的是 “开放定址法” 处理hash冲突, 具体行为是把 HashOf(k) % Array.Length 改为 (HashOf(k) + d(k)) % Array.Length , 得出另外一个位置来存储key 所对应的数据, d() 是一个增量函数. 如果仍然冲突, 则再次进行增量, 依此循环直到找到一个 Array 中的空位为止。

负载因子0.72,当元素个数超过0.72 * Capacity时,就要扩容,扩容为两倍于原先容量最小的素数:3->7

Dictionary

可以将Dictionary理解为拥有泛型的Hashtable, 键值对类型从Hashtable的object变为了可以自己制定的泛型。

  • [1] 单线程程序中推荐使用 Dictionary, 有泛型优势, 且读取速度较快, 容量利用更充分.
  • [2] 多线程程序中推荐使用 Hashtable, 默认的 Hashtable 允许单线程写入, 多线程读取, 对 Hashtable 进一步调用 Synchronized() 方法可以获得完全线程安全的类型. 而 Dictionary 非线程安全, 必须人为使用 lock 语句进行保护, 效率大减.
  • [3] Dictionary 有按插入顺序排列数据的特性 (注: 但当调用 Remove() 删除过节点后顺序被打乱), 因此在需要体现顺序的情境中使用 Dictionary 能获得一定方便.
  • Dictionary<K,V>是泛型的,当K或V是值类型时,其速度远远超过Hashtable。(避免了装箱拆箱)

我认为应该始终使用Dictionary<K, V>,即使要用Hashtable了,也可以用Dictionary<object, object>来替代。