在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
类(Class) 最常见的一种引用类型 class YourClassName { } class前面可以加上一些关键字,比如public、private、abstract、static、readonly class后面是这个类的名称,类型名称后面也可以跟一些东西,比如泛型类、继承的父类等
字段(Fields) 是Class或Struct的成员,它是一个变量 class Octopus { string name; public int Age = 10; } readonly修饰符
字段的初始化
同时声明多个字段 static readonly int legs = 8, eyes = 2;
方法
方法的签名 类型类方法的签名必须唯一 签名:方法名、参数类型(含顺序,但与参数名称和返回类型无关)
Expression-bodied methods (C# 6) int Foo (int x) int Foo (int x) => x * 2; void Foo (int x) => Console.WriteLine (x); 第一个是一个常规的写法,第二个就是第一个的Expression-bodied方法的形式。当然没有返回类型的也可以这样写,这种写法只适用于一些简单的方法,单表达式的方法。 方法的重载(Overloading methods) 类型里的方法可以进行重载(允许多个同名的方法同时存在),只要这些方法的签名不同就行 void Foo (int x) {...} void Foo (double x) {...} void Foo (int x, float y) {...} void Foo (float x, int y) {...} 在看一个特殊点的方法的例子 void Foo (int x) {...} float Foo (int x) {...} // 编译时报错,签名相同,因为签名不包含方法的返回类型 void Goo (int[] x) {...} void Goo (params int[] x) {...} // 编译时错误,签名相同,因为签名不包含参数类型的修饰符(params去掉就是一样的) 按值传递 和 按引用传递 参数是按值传递的还是按引用传递的,也是方法签名的一部分 void Foo (int x) {...} void Foo (ref int x) {...} // 是可以的 因为参数的传递方式是不同的,一个是值类型,一个是引用类型,所以签名不一致,是可以的,下面这样就不行了: void Foo (int x) {...} void Foo (ref int x) {...} // 是可以的 void Foo (out int x) {...} // 编译时错误 后两个方法的参数类型和方法名都是一样的,而且都是按引用传递,所以后两个的签名是一样的 本地方法(C# 7) 一个方法在另一个方法的里面,就叫本地方法(Local methods) void WriteCubes() { Console.WriteLine (Cube (3)); Console.WriteLine (Cube (4)); Console.WriteLine (Cube (5)); int Cube (int value) => value * value * value; }
构造函数
看一个构造的例子: public class Panda { string name; // 定义字段 public Panda (string n) // 定义构造函数 { name = n; // 初始化代码 } } …… Panda p = new Panda ("Petey"); // 调用构造函数 构造函数允许以下修饰符
从C#7开始,允许单语句的构造函数写成expression-bodied成员的形式,上面的Panda类的构造函数可以写成如下形式: public Panda (string n) => name = n;
构造函数的重载
看下例子: public class Wine { public decimal Price; public int Year; public Wine (decimal price) { Price = price; } public Wine (decimal price, int year) : this (price) { Year = year; } }
看下下面的例子: public class Wine { public decimal Price; public int Year; public Wine (decimal price) { Price = price; Console.WriteLine("1"); } public Wine (decimal price, int year) : this (price) { Year = year; Console.WriteLine("2"); } } 调用构造函数var wine = new Wine(25m, 1988); 结果是先输出1,再输出2
无参构造函数
构造函数和字段的初始化顺序
非public的构造函数 构造函数可以不是public的,看个例子: public class Class1 { Class1() {} // 私有的构造函数 public static Class1 Create (...) { // 在这里执行自定义的逻辑来返回Class1的实例 ... } } 那么获取类的实例则无法在通过new关键字获取了,而是通过类内部的创建实例的静态方法获取:var instance = Class1.Create(); 单例模式则可以这么写
Deconstructors (C# 7) C#7引入了deconstructor模式,作用基本和构造函数相反,它会把字段反赋给一堆变量 有一些要求,比如方法名必须是Deconstruct,有一个或多个out参数 class Rectangle { public readonly float Width, Height; public Rectangle (float width, float height) { Width = width; Height = height; } public void Deconstruct (out float width, out float height) { width = Width; height = Height; } } 调用析构函数,我们可以下面的方式 var rect = new Rectangle (3, 4); (float width, float height) = rect; // Deconstruction Console.WriteLine (width + " " + height); // 3 4 第二行就是调用析构函数,当然,它也可以这么写,似乎更好理解一点 float width, height; rect.Deconstruct (out width, out height); 或者以下的方法都是可以的: rect.Deconstruct (out var width, out var height); //或者这样 (var width, var height) = rect; //或这样 var (width, height) = rect; //或这样 float width, height; (width, height) = rect; 析构函数,可以重载,也可以是扩展方法,扩展方法这里就不展开说了
对象初始化器(Object Initializers) 对象任何可访问的字段/属性在构造之后,可通过对象初始化器直接为其进行设定值 下面是一个Bunny类,包含一个无参的构造函数和一个有参的构造函数 public class Bunny { public string Name; public bool LikesCarrots; public bool LikesHumans; public Bunny () {} public Bunny (string n) { Name = n; } } 使用对象初始化器实例化Bunny对象 // 无参的构造函数可以省略掉小括号 Bunny b1 = new Bunny { Name="Bo", LikesCarrots=true, LikesHumans=false }; Bunny b2 = new Bunny ("Bo") { LikesCarrots=true, LikesHumans=false }; 上面的这个对象初始化器的代码,就相当于下面的这段代码: Bunny temp1 = new Bunny(); // temp1 是编译器生成的名称 temp1.Name = "Bo"; temp1.LikesCarrots = true; temp1.LikesHumans = false; Bunny b1 = temp1; Bunny temp2 = new Bunny ("Bo"); temp2.LikesCarrots = true; temp2.LikesHumans = false; Bunny b2 = temp2; 临时变量是为了确保如果在初始化过程中发生了异常,那么不会以一个初始化了一半的对象而结束
对象初始化器与可选参数 如果不使用初始化器,上面例子中的构造函数也可以使用可选参数: public Bunny (string name, bool likesCarrots = false, bool likesHumans = false) { Name = name; LikesCarrots = likesCarrots; LikesHumans = likesHumans; } 那么在调用的时候可以这么调用,name是必须的,后面两个参数可传可不传: Bunny b1 = new Bunny (name: "Bo", likesCarrots: true); 可选参数方式有个优点,可以做到让Bunny类的字段/属性只读,也有个缺点,每个可选参数的值都被嵌入到了调用栈(calling site),C#会把构造函数的调用翻译成下面的形式: Bunny b1 = new Bunny ("Bo", true, false); 其实我们使用了两个参数,最后一个值我们没有用,但是它把这个值嵌入进去(默认值) 这个可选参数的缺点,有时候可能会引起一些问题,比如,我们在另一个程序集里实例化这个类,然后为这个Bunny再添加了一个参数,变成4个参数了,这个时候就可能有问题,因为如果另一个程序集没有重新编译,那么调用的就还是原来的三个参数的构造函数,然后一运行运行时就会报错了;还有一种情况是,如果改变了其中一个可选参数的值,那么其他程序集的调用者调用的还是会使用原来旧的参数值,直到那些程序集被重新编译。
this 引用 this引用指的是实例的本身 public class Panda { public Panda Mate; public void Marry (Panda partner) { Mate = partner; partner.Mate = this; } } this引用可以让你把字段与本地变量或参数区分开 public class Test { string name; public Test (string name) { this.name = name; } } 只有class/struct的非静态成员才可以使用this
属性(Properties) 从外部来看,属性和字段很像,但从内部看,属性含有逻辑,就像方法一样 Stock msft = new Stock(); msft.CurrentPrice = 30; msft.CurrentPrice -= 3; Console.WriteLine (msft.CurrentPrice); 我们从外面看不出来CurrentPrice是属性还是字段 属性的声明 属性的声明和字段的声明很像,但是多了一个get、set块 public class Stock { decimal currentPrice; // The private "backing" field public decimal CurrentPrice // The public property { get { return currentPrice; } set { currentPrice = value; } } } 属性的get set
属性与字段的区别 尽管属性的访问方式与字段的访问方式相同,但不同之处在于,属性赋予了实现者对获取和赋值的完全控制权。这种控制允许实现者选择任意所需的内部表示,不向属性的使用者公开其内部实现细节。
只读和计算的属性 如果属性只有get访问器,那么它就是只读的 如果属性只有set访问器,那么它就是只写的(很少这样用) 属性通常拥有一个专用的“幕后”字段(backing field),这个幕后的字段用来存储数据 decimal currentPrice, sharesOwned; public decimal Worth { get { return currentPrice * sharesOwned; } }
Expression-bodied 属性 从C#6开始,你可以使用Expression-bodied形式来表示只读属性 public decimal Worth => currentPrice * sharesOwned; C#7,允许set访问器也可以使用这种形式 public decimal Worth { get => currentPrice * sharesOwned; set => sharesOwned = value / currentPrice; }
自动属性 属性最常见的一种实践是:setter和getter只是对private field进行简单的直接的读写。自动属性声明就告诉编译器来提供这种实现 public class Stock { ... public decimal CurrentPrice { get; set; } } 编译器会自动生成一个幕后的私有字段,其名称不可引用 set访问器也可以是private或protected
属性初始化器 从C#6开始,你可以为自动属性添加属性初始化器 public decimal CurrentPrice { get; set; } = 123; 只读的自动属性也可以使用(只读自动属性也可以在构造函数里被赋值) public int Maximum { get; } = 999;
get 和 set 的访问性 get和set访问器可以拥有不同的访问级别,典型的用法:public get,internal/private set public class Foo { private decimal x; public decimal X { get { return x; } private set { x = Math.Round (value, 2); } } } 注意,通常属性的访问级别更宽松一些,访问器的访问级别更严一些(上面例子中的属性X的public和set的private)
CLR的属性实现 C#的属性访问器内部会编译成get_XXX 和 set_XXX public decimal get_CurrentPrice {...} public void set_CurrentPrice (decimal value) {...} 简单的非virtual属性访问器会被JIT编译器进行内联(inline)操作,这会消除访问属性与访问字段之间的性能差异。内联是一种优化技术,它会把方法调用换成直接使用方法体。
索引器 索引器提供了一种可以访问封装了列表值或字典值的class/struct的元素的一种自然的语法 string s = "hello"; Console.WriteLine (s[0]); // 'h' Console.WriteLine (s[3]); // 'l' 语法很像使用数组时用的语法,但是这里的索引参数是可以是任何类型的 索引器和属性拥有同样的修饰符 可以按照下列方式使用null条件操作符: string s = null; Console.WriteLine (s?[0]); 实现索引器 需要定义一个this属性,并通过中括号指定参数 class Sentence { string[] words = "The quick brown fox".Split(); public string this [int wordNum] // 索引器 { get { return words [wordNum]; } set { words [wordNum] = value; } } } 使用索引器 Sentence s = new Sentence(); Console.WriteLine (s[3]); // fox s[3] = "kangaroo"; Console.WriteLine (s[3]); // kangaroo 多个索引器 一个类型可以声明多个索引器,它们的参数类型可以不同 一个索引器可以有多个参数 public string this [int arg1, string arg2] { get { ... } set { ... } } 只读索引器 如果不写set访问器,那么这个索引器就是只读的 在C#6以后,也可以使用Expression-bodied语法 public string this [int wordNum] => words [wordNum]; CLR的索引器实现 索引器在内部会编译成get_Item和set_Item方法 public string get_Item (int wordNum) {...} public void set_Item (int wordNum, string value) {...}
常量
一个常量的例子: public class Test { public const string Message = "Hello World"; } 常量与静态只读字段 常量比静态只读字段更严格: 体现在两个方面,一个是可使用的类型,第二个是字段初始化的语义上 常量是在编译时进行值得估算的 public static double Circumference (double radius) { return 2 * System.Math.PI * radius; } 编译成下面的形式: public static double Circumference (double radius) { return 6.2831853071795862 * radius; } 有一点需要注意,当值有可能改变,并且需要暴露给其它程序集的时候,静态只读字段是相对较好的选择 public const decimal ProgramVersion = 2.3; 如果Y 程序集引用了X程序集并且使用了这个常量,那么在编译的时候,2.3这个值就会固化在Y程序集里,这意味着,如果后来X重新编译了,这个常量变成了2.4,如果Y不重新编译的话,Y仍将使用2.3这个值,直到Y被重新编译,它的值才会变成2.4.静态只读字段就会避免这个问题的发生。 常量不仅仅可以作为类型的字段,它还可以作为本地常量,在方法里声明常量,例如: static void Main() { const double twoPI = 2 * System.Math.PI; ... }
静态构造函数
class Test { static Test() { Console.WriteLine ("Type Initialized"); } }
一个是实例化一个类型之前,一个是访问类型的一个静态成员之前
如果静态构造函数跑出来未处理的异常,那么这个类型在该程序的剩余生命周期内将无法使用了
静态构造函数和字段初始化顺序
class Foo { public static int X = Y; // 0 public static int Y = 3; // 3 } 因为是按照声明的顺序初始化的,所以X的时候Y还没有,所以是0。 class Program { static void Main() { Console.WriteLine (Foo.X); } // 3 } class Foo { public static Foo Instance = new Foo(); public static int X = 3; Foo() { Console.WriteLine (X); } // 0 } 在类Program里,输出的是3,因为在使用Foo类之前这个类型就已经初始化完成,所以是3,而在Foo类里面,则按照什么的先后顺序分别初始化静态构造函数,然后是静态字段,所以静态构造函数输出的是0,X还没有初始化。
静态类 类也可以是静态的,但是有个要求,就是其成员也必须全是静态的,不可以有子类。 例如:System.Console 、System.Math 等
终结器(Finalizers) Finalizer是class专有的一种方法,在GC回收未引用对象的内存之前运行,其实就是对object的Finalize()方法重写的一种语法 这个很少用,例子: class Class1 { ~Class1() { ... } } 编译生成的是这样的 protected override void Finalize() { ... base.Finalize(); } C#7可以这样写: ~Class1() => Console.WriteLine ("Finalizing");
|
请发表评论