类型与变量
C# 的五大数据类型
类(class
)
结构体(struct
)
枚举(enum
)
接口(interface
)
委托(Delegates
)
所有类型都由 Object
派生。
什么是变量
变量就是以变量名所对应的内存地址为起点,以其数据类型所要求的存储空间为长度的一块内存区域。
变量的用途是存储数据,变量表示了存储位置,并且每个变量都有一个类型,以决定什么样的值能够存入变量。
变量的分类
变量一共有 7 种:静态变量,实例变量(成员变量、字段),数组元素,值参数,引用参数(ref
修饰),输出形参(out
修饰),局部变量。
值变量(byte/int/short/ushort...
)没有实例,即所谓的实例与变量合而为一。引用类型变量存储的是对象的内存地址。引用参数作为实参的别名,对引用参数的改变都能在实参中体现;与引用参数不同,在方法内部,输出参数在能够被读取之前必须被赋值,这意味着参数的初始值是无关的,而且没有必要在方法调用之前为实参赋值, 方法返回之前,方法内部贯穿的任何可能路径都必须为所有输出参数进行一次赋值。
变量内存分配
值变量的实例与变量合而为一,直接按照数据类型所需比特数分配内存,高地址内存对应值的高位。
程序创建引用变量,计算机立刻分配 4 个字节用于存储实例的地址,将每一位置 0,表示引用变量没有引用任何实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 namespace Sample { class Program { static void Main (string [] args ) { Student student; student = new Student(); } } class Student { public uint ID; public ushort score; } }
局部变量在栈中分配内存,实例变量在堆中分配内存。C# 因此还存在装箱与拆箱 的概念:当发现 obj
引用的对象是栈上的值变量 x
时,先将 x
在堆中拷贝一份,构成对堆中实例的引用,称此过程为装箱;当局部变量获取 obj
引用的值时,将堆中数据拷贝到栈中,称此过程为拆箱。可以看到,装箱拆箱的过程比较损耗性能。
1 2 3 4 5 6 7 8 9 10 11 namespace Sample { class Program { static void Main (string [] args ) { const int x = 100 ; object obj = x; int y = (int )obj; } } }
类型转换
隐式(implicit)转换
1 Exception e = new StackOverflowException();
显示(explicit)转换
1 2 int x = ushort .MaxValue + 1 ;ushort y = (ushort )x;
1 int x = Convert.ToInt32(false );
ToString 方法与各数据类型的 Parse/TryParse 方法
自定义类型转换操作符
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 namespace Sample { class Program { static void Main (string [] args ) { Stone stone = new Stone() { age = 5000 }; Monkey wukongSun = (Monkey)stone; Console.WriteLine(wukongSun.age); } } class Stone { public int age; public static explicit operator Monkey (Stone stone ) { Monkey monkey = new Monkey { age = stone.age / 500 }; return monkey; } } class Monkey { public int age; } }
方法
方法的由来
方法(method)的前身是 C/C++ 的函数(function)。C# 中方法不可能独立于类或结构体之外,而 C/C++ 中有全局函数。
“ 为什么需要方法?” 这个问题可这样回答: 隐藏复杂的逻辑,把大算法分解为小算法,利用复用和重用实现复杂功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 namespace Sample { class Calculator { public double GetCirCleArea (double r ) { return Math.PI * r * r; } public double GetCylinderVolume (double r,double h ) { return GetCirCleArea(r) * h; } public double GetConeVolue (double r,double h ) { return GetCylinderVolume(r, h) / 3.0 ; } } }
构造器
构造器是类型的成员之一,狭义的构造器是实例构造器。
程序员定义构造器后,默认构造器就不存在。
方法重载
方法签名由方法的名称、类型形参的个数和它的每一个形参(按从左到右的顺序)的类型和种类(值、引用或输出)组成。方法签名不包含返回类型 。
实例构造函数签名由它的每一个形参(按从左到右的顺序)的类型和种类(值、引用或输出)组成。
重载决策︰用于在给定了参数列表和一组候选函数成员的情况下,选择一个最佳函数成员来实施调用。
对方法进行 Debug
设置断点
观察方法调用时的调用堆栈(Call Stack)
逐语句(Step-in),逐过程(Step-over),跳出(Step-out)
观察局部变量的值与变化
方法的调用与栈
调用方法的方法负责压栈。在下面的例子中,Main
调用 GetCircleArea
,负责将两个 double
压栈,并记录各种地址信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 namespace Sample { class Program { static void Main (string [] args ) { Calculator calculator = new Calculator(); Console.WriteLine(calculator.GetCirCleArea(4 )); } } class Calculator { public double GetCirCleArea (double r ) { return Math.PI * r * r; } } }
方法的返回值一般存储在寄存器中,而不是栈内存中。当方法返回时,会将方法占用的内存释放。
扩展方法
扩展方法就是为 C# 内置的数据类型设计额外的方法。
扩展方法必须是共有的、静态的。
形参列表中的第一个参数必须用 this 修饰。
扩展方法必须由一个静态类来统一收纳。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 namespace Sample { class Program { static void Main (string [] args ) { const double x = Math.PI; Console.WriteLine(x.Round(11 )); } } static class DoubleExtension { public static double Round (this double input,int digits ) { return Math.Round(input, digits); } } }
操作符的原理和使用
操作符的本质
操作符的本质是函数的简记法。
操作符不能脱离与它关联的数据类型。
操作符举例
1 2 3 4 5 6 7 8 9 10 11 12 13 namespace Sample { class Program { static void Main (string [] args ) { Type type = typeof (string ); foreach (var method in type.GetMethods()) { Console.WriteLine(method.Name); } } } }
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 namespace Sample { enum Level1 { Mid, Low, High } enum Level2 { Mid = 1 , Low = 0 , High = 2 } enum Level3 { Mid = 2 , Low = 1 , High = 3 } class Program { static void Main (string [] args ) { Console.WriteLine(default (int )); Console.WriteLine(default (Level1)); Console.WriteLine(default (Level2)); Console.WriteLine(default (Level3)); Console.WriteLine(default (Form) == null ); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 namespace Sample { class Program { static void Main (string [] args ) { uint x = uint .MaxValue; try { uint y = checked(x + 1 ); Console.WriteLine(y); } catch (OverflowException e) { Console.WriteLine(e.Message); } } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 namespace Sample { class Program { static void Main (string [] args ) { Console.WriteLine(sizeof (decimal )); unsafe { Console.WriteLine(sizeof (Student)); } } } struct Student { ulong ID; ulong Score; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 namespace Sample { class Program { static void Main (string [] args ) { unsafe { Student stu; Student* pStu = &stu; pStu->ID = 1 ; pStu->Score = 100 ; Console.WriteLine(pStu->Score); } } } struct Student { public ulong ID; public ulong Score; } }
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 namespace Sample { class Program { static void Main (string [] args ) { Teacher teacher = new Teacher(); Console.WriteLine(teacher is Animal); Car car = new Car(); Console.WriteLine(car is Teacher); Human human = new Human(); Console.WriteLine(human is Teacher); } } class Animal { public void Eat () { Console.WriteLine("Eating..." ); } } class Human : Animal { public void Think () { Console.WriteLine("Who I am?" ); } } class Teacher : Human { public void Teach () { Console.WriteLine("I teach programming." ); } } class Car { public void Run () { Console.WriteLine("Running..." ); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 namespace Sample { class Program { static void Main (string [] args ) { Object obj = new Animal(); Animal animal = obj as Animal; if (animal != null ) { animal.Eat(); } } } class Animal { public void Eat () { Console.WriteLine("Eating..." ); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 namespace Sample { class Program { static void Main (string [] args ) { int ? x = null ; int y = x ?? 1 ; Console.WriteLine(y); } } }
字段与属性
字段
字段的概念
字段(field)是一种表示与对象或类型(类与结构体)关联的变量。字段是类型的成员,旧称“成员变量”。与对象关联的字段亦称"实例字段”,与类型关联的字段称为“静态字段”,由 static 修饰。
字段的初始化
无显式初始化时,字段获得其类型的默认值,所以字段“永远都不会未被初始化”。实例字段初始化的时机是对象创建时,静态字段初始化的时机是类型被加载(load)时。
属性
属性的概念
属性(property)是一种用于访问对象或类型的特征的成员,特征反映了状态。属性是字段的自然扩展。
从命名上看,字段更偏向于实例对象在内存中的布局,属性更偏向于反映现实世界对象的特征。对外,属性暴露数据,数据可以是存储在字段里的,也可以是动态计算出来的;对内,属性保护字段不被非法值“污染”。属性由 Get/Set 方法对进化而来。
属性的声明
完整声明——后台(back )成员变量与访问器(注意使用 code snippet 和 refactor 工具)。
简略声明——只有访问器(查看 IL 代码)。
动态计算值的属性。
注意实例属性和静态属性属性的名字一定是名词。
只读属性——只有 getter 没有 setter。
尽管语法上正确,几乎没有人使用“只写属性”,因为属性的主要目的是通过向外暴露数据而表示对象或类型的状态。
属性与字段的关系
—般情况下,它们都用于表示实体(对象或类型)的状态。属性大多数情况下是字段的包装器(wrapper)。
建议永远使用属性而不是字段来暴露数据,即字段永远都是 private 或 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 29 30 31 32 33 34 namespace Sample { class Program { static void Main (string [] args ) { try { Student student = new Student { Age = 200 }; } catch (Exception e) { Console.WriteLine(e.ToString()); } } } class Student { private int age; public int Age { get { return this .age; } set { if (value < 0 || value > 120 ) { throw new Exception("年龄不合法" ); } else { this .age = value ; } } } } }
索引器概述
什么是索引器
索引器(indexer)是对象的成员,它使得对象能够使用与数组相同的方式(即使用下标)进行索引。
自定义索引器
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 namespace Sample { class Program { static void Main (string [] args ) { try { Student student = new Student(); student["Math" ] = 100 ; Console.WriteLine(student["Math" ]); } catch (Exception e) { Console.WriteLine(e.ToString()); } } } class Student { private Dictionary<string , int > scoreDic = new Dictionary<string , int >(); public int ? this [string subject] { get { if (scoreDic.ContainsKey(subject)) { return scoreDic[subject]; } else { return null ; } } set { if (!value .HasValue) { throw new Exception("成绩不能为空!" ); } if (scoreDic.ContainsKey(subject)) { scoreDic[subject] = value .Value; } else { scoreDic.Add(subject, value .Value); } } } } }
委托
什么是委托
委托是函数指针的 “升级版”。
使用 C# 内置类定义委托
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 namespace Sample { class Program { static void Main (string [] args ) { Calculator calculator = new Calculator(); Action action = new Action(calculator.Report); action(); Func<int , int , int > func1 = new Func<int , int , int >(calculator.Add); Func<int , int , int > func2 = new Func<int , int , int >(calculator.Sub); Console.WriteLine(func1(4 ,3 )); Console.WriteLine(func2(5 ,3 )); } } class Calculator { public void Report () { Console.WriteLine("I have 3 methods" ); } public int Add (int a,int b ) { return a + b; } public int Sub (int a,int b ) { return a - b; } } }
自定义委托
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 namespace Sample { public delegate int Calc (int x, int y ) ; class Program { static void Main (string [] args ) { Calculator calculator = new Calculator(); const int a = 100 ; const int b = 10 ; Console.WriteLine((new Calc(calculator.Add)(a,b))); Console.WriteLine((new Calc(calculator.Sub)(a, b))); } } class Calculator { public void Report () { Console.WriteLine("I have 3 methods" ); } public int Add (int a,int b ) { return a + b; } public int Sub (int a,int b ) { return a - b; } } }
委托的使用
模板方法
使用模板方法一般是一部分代码“借用”指定的外部方法(以委托方式传入)来产生结果。这样的方式相当于 “填空题”,常位于代码中部,委托一般有返回值。
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 namespace Sample { class Program { static void Main (string [] args ) { ProductFactory productFactoruy = new ProductFactory(); WrapFactory wrapFactory = new WrapFactory(); Func<Product> func1 = new Func<Product>(productFactoruy.MakePizza); Func<Product> func2 = new Func<Product>(productFactoruy.MakeToyCar); Box box1 = wrapFactory.WrapProduct(func1); Box box2 = wrapFactory.WrapProduct(func2); Console.WriteLine(box1.Product.Name); Console.WriteLine(box2.Product.Name); } } class Product { public string Name { get ; set ; } } class Box { public Product Product { get ; set ; } } class WrapFactory { public Box WrapProduct (Func<Product> getProduct ) { return new Box() { Product = getProduct() }; } } class ProductFactory { public Product MakePizza () { return new Product() { Name = "Pizza" }; } public Product MakeToyCar () { return new Product() { Name = "Toy Car" }; } } }
回调方法
主调方法接受委托类型参数,调用回调方法。这样的方式相当于“流水线”,常位于代码末尾,委托一般无返回值。
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 73 74 75 76 77 78 namespace Sample { class Program { static void Main (string [] args ) { ProductFactory productFactoruy = new ProductFactory(); WrapFactory wrapFactory = new WrapFactory(); Func<Product> func1 = new Func<Product>(productFactoruy.MakePizza); Func<Product> func2 = new Func<Product>(productFactoruy.MakeToyCar); Logger logger = new Logger(); Action<Product> log = new Action<Product>(logger.Log); Box box1 = wrapFactory.WrapProduct(func1,log); Box box2 = wrapFactory.WrapProduct(func2,log); Console.WriteLine(box1.Product.Name); Console.WriteLine(box2.Product.Name); } } class Logger { public void Log (Product product ) { Console.WriteLine("Product '{0}' created at {1}. Price is {2}" , product.Name, DateTime.UtcNow, product.Price); } } class Product { public string Name { get ; set ; } public decimal Price { get ; set ; } } class Box { public Product Product { get ; set ; } } class WrapFactory { public Box WrapProduct (Func<Product> getProduct, Action<Product> logCallback ) { Product product = getProduct(); if (product.Price > 50 ) { logCallback(product); } return new Box { Product = product }; } } class ProductFactory { public Product MakePizza () { return new Product() { Name = "Pizza" , Price = 12 }; } public Product MakeToyCar () { return new Product() { Name = "Toy Car" , Price = 100 }; } } }
多播委托
用一个委托封装多个方法。
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 namespace Sample { class Program { static void Main (string [] args ) { Student student1 = new Student() { ID = 1 , PenColor = ConsoleColor.Yellow }; Student student2 = new Student() { ID = 2 , PenColor = ConsoleColor.Green }; Student student3 = new Student() { ID = 3 , PenColor = ConsoleColor.Red }; Action action1 = new Action(student1.DoHomework); Action action2 = new Action(student2.DoHomework); Action action3 = new Action(student3.DoHomework); action1 += action2; action1 += action3; action1(); } } class Student { public int ID { get ; set ; } public ConsoleColor PenColor { get ; set ; } public void DoHomework () { for (int i = 0 ; i < 5 ; i++) { Console.ForegroundColor = PenColor; Console.WriteLine("Student {0} has been doing homework for {1} hour(s)." , ID, i); Thread.Sleep(1000 ); } } } }
隐式异步调用
用委托开启多线程。
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 namespace Sample { class Program { static void Main (string [] args ) { Student student1 = new Student() { ID = 1 , PenColor = ConsoleColor.Yellow }; Student student2 = new Student() { ID = 2 , PenColor = ConsoleColor.Green }; Student student3 = new Student() { ID = 3 , PenColor = ConsoleColor.Red }; Action action1 = new Action(student1.DoHomework); Action action2 = new Action(student2.DoHomework); Action action3 = new Action(student3.DoHomework); action1.BeginInvoke(null , null ); action2.BeginInvoke(null , null ); action3.BeginInvoke(null , null ); Thread.Sleep(10000 ); } } class Student { public int ID { get ; set ; } public ConsoleColor PenColor { get ; set ; } public void DoHomework () { for (int i = 0 ; i < 5 ; i++) { Console.ForegroundColor = PenColor; Console.WriteLine("Student {0} has been doing homework for {1} hour(s)." , ID, i); Thread.Sleep(1000 ); } } } }
注意委托的使用
应当适时地使用接口取代一些对委托的使用。
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 namespace Sample { class Program { static void Main (string [] args ) { IProductFactory pizzaFactory = new PizzaFactory(); IProductFactory toyCarFactory = new ToyCarFactory(); WrapFactory wrapFactory = new WrapFactory(); Box box1 = wrapFactory.WrapProduct(pizzaFactory); Box box2 = wrapFactory.WrapProduct(toyCarFactory); Console.WriteLine(box1.Product.Name); Console.WriteLine(box2.Product.Name); } } interface IProductFactory { Product Make () ; } class PizzaFactory : IProductFactory { public Product Make () { return new Product() { Name = "Pizza" }; } } class ToyCarFactory : IProductFactory { public Product Make () { return new Product() { Name = "ToyCar" }; } } class Product { public string Name { get ; set ; } } class Box { public Product Product { get ; set ; } } class WrapFactory { public Box WrapProduct (IProductFactory productFactory ) { return new Box() { Product = productFactory.Make() }; } } }
对于难精通、易使用、功能强大的工具,一旦被滥用后果很严重。
这是一种方法级别的紧耦合,现实工作中要慎之又慎。
委托使代码可读性下降,debug 的难度增加。
把委托回调、异步调用和多线程纠缠在一起,会让代码变得难以阅读和维护。
委托使用不当有可能造成内存泄漏和程序性能下降。
事件
初步了解事件
事件的概念
事件在英文中为 Event,大致意思为“会发生的重要的事”。
事件扮演的角色
事件是一种使对象或类具备通知能力的成员,类似消息响应。
事件的本质是委托字段的一个包装器,这个包装器对委托字段的访问起到限制作用,相当于一个“蒙版”。
事件对外界隐藏了委托实例的大部分功能,仅暴露添加和移除事件处理器的功能。
事件模型的组成部分
事件的拥有者(event source,对象)
事件成员(event,成员)
事件的响应者(event subsciber,对象)
事件处理器(event handler,成员)
事件订阅
事件的现实应用
事件多用于桌面、手机等开发的客户端编程,因为这些程序经常是用户通过事件来“驱动”的。
各种编程语言对这个机制的实现方法不尽相同。
Java 没有事件成员,也没有委托这种数据类型,Java的“事件"是使用接口来实现的。
MVC、MVP、MVM 等模式,是事件模式更高级、更有效的"玩法”。
日常开发的时候,使用已有事件的机会比较多,自己声明事件的机会比较少,所以先学使用。
事件的应用
注意
事件处理器是方法成员。
挂接事件处理器的时候,可以使用委托实例,也可以使用直接方法名,这是个“语法糖”。
事件处理器对事件的订阅不是随意的,匹配与否由声明事件时所使用的委托类型来检测。
事件可以同步调用也可以异步调用。
声明自定义事件
自定义事件实际上就是声明事件处理的委托,然后给委托指明处理事件的方法。
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 73 74 75 76 77 78 79 namespace Sample { class Program { static void Main (string [] args ) { Customer customer = new Customer(); Waiter waiter = new Waiter(); customer.Order += waiter.Action; customer.Action(); customer.PayTheBill(); } } public class OrderEventArgs { public string DishName { get ; set ; } public string Size { get ; set ; } } public delegate void OrderEventHandler (Customer customer, OrderEventArgs e ) ; public class Customer { public double Bill { get ; set ; } public void PayTheBill () { Console.WriteLine("I will pay ${0}" ,this .Bill); } public void WalkIn () { Console.WriteLine("Walk into the restaurant." ); } public void SitDown () { Console.WriteLine("Sit down." ); } public void Think () { for (int i = 0 ; i < 5 ; i++) { Console.WriteLine("Let me think..." ); Thread.Sleep(1000 ); } orderEventHandler?.Invoke(this , new OrderEventArgs() { DishName = "Chicken" , Size = "large" }); } public void Action () { Console.ReadKey(); WalkIn(); SitDown(); Think(); } private OrderEventHandler orderEventHandler; public event OrderEventHandler Order { add { orderEventHandler += value ; } remove { orderEventHandler -= value ; } } } public class Waiter { public void Action (Customer customer, OrderEventArgs e ) { Console.WriteLine("I will serve you the dish - {0}" , e.DishName); double price = 10 ; switch (e.Size) { case "small" : price *= 0.5 ; break ; case "large" : price *= 1.5 ; break ; default : break ; } customer.Bill += price; } } }
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 73 74 75 76 77 namespace Sample { class Program { static void Main (string [] args ) { Customer customer = new Customer(); Waiter waiter = new Waiter(); customer.Order += waiter.Action; customer.Action(); customer.PayTheBill(); } } public class OrderEventArgs : EventArgs { public string DishName { get ; set ; } public string Size { get ; set ; } } public delegate void OrderEventHandler (Customer customer, OrderEventArgs e ) ; public class Customer { public double Bill { get ; set ; } public void PayTheBill () { Console.WriteLine("I will pay ${0}" ,this .Bill); } public void WalkIn () { Console.WriteLine("Walk into the restaurant." ); } public void SitDown () { Console.WriteLine("Sit down." ); } public void Think () { for (int i = 0 ; i < 5 ; i++) { Console.WriteLine("Let me think..." ); Thread.Sleep(1000 ); } Order?.Invoke(this , new OrderEventArgs() { DishName = "Chicken" , Size = "large" }); } public void Action () { Console.ReadKey(); WalkIn(); SitDo wn () ; Think(); } public event OrderEventHandler Order; } public class Waiter { public void Action (Customer customer, OrderEventArgs e ) { Console.WriteLine("I will serve you the dish - {0}" , e.DishName); double price = 10 ; switch (e.Size) { case "small" : price *= 0.5 ; break ; case "large" : price *= 1.5 ; break ; default : break ; } customer.Bill += price; } } }
事件的必要性
在上面的事件示例代码中,我们完全可以使用委托来实现同样的效果:我们声明委托字段 public OrderHandler Order
,将 waiter.Action
挂到委托上即可。不禁要问:有了委托字段,为什么还需要引入事件?
原因是,为了程序的逻辑更加符合实际、更加安全,我们必须引入事件,谨防 “借刀杀人”。在上面的实例中,如果使用委托实现,我们可以在调用委托时填入参数 ,加入另外一个 Customer’ 需要点菜,而将消息的发送者(sender)参数写为 Customer,那么就 “把自己的账算到了别人的头上”,这种情况显然是不安全的;而事件不可直接调用,只能放在 +=
或 -=
的左边,仅暴露添加或删除 事件处理器的功能,事件触发必须由事件的拥有者来做 。
类
什么是类
类是一种数据结构。
类是一种数据类型。
类代表现实世界中的“种类”。
类的访问控制
public class
,类能被其他项目访问。
internal class
,类仅能被当前程序集访问,默认为 internal。
private class
,只能在类内嵌套类时使用。
类的继承
重写与隐藏
1 2 3 4 5 6 7 8 9 10 11 class Vehicle { internal protected virtual void Run () { Console.WriteLine("I'm running!" ); } } class Car : Vehicle { internal protected override void Run () { Console.WriteLine("Car is running!" ); } }
1 2 3 4 5 6 7 8 9 10 11 class Vehicle { internal protected void Run () { Console.WriteLine("I'm running!" ); } } class Car : Vehicle { internal protected new void Run () { Console.WriteLine("Car is running!" ); } }
注意事项
类在功能上进行扩展(extend)。
只能有一个基类,但可以实现有多个基接口。
类访问级别对继承有影响。
sealed 类不能被继承。
首先调用父类构造器,再调用子类构造器。
接口
抽象类
抽象类是未完全实现逻辑的类,抽象类不允许被实例化。
抽象类可作为基类被继承,可用抽象类的变量引用子类的实例。抽象类为复用而生,撞门作为基类来使用,也具有解耦功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 abstract class Vehicle { public abstract void Run () ; public void Stopped () { Console.WriteLine("Stopped!" ); } } class Car :Vehicle { public override void Run () { Console.WriteLine("Car is running..." ); } } class Truck : Vehicle { public override void Run () { Console.WriteLine("Truck is running..." ); } }
开闭原则
我们应该封装不变的、稳定的、确定的成员,而把不确定的、有可能改变的成员,声明为抽象成员,留给子类去实现。
接口简介
接口是完全未实现逻辑的 “类”,只有成员函数,成员全部为 public。
接口为解耦而生,实现 “高内聚,低耦合”,方便单元测试。
接口是一个 “协约”,早已为工业生产所熟知。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 interface IVehicle { void Run () ; void Stop () ; } abstract class Vehicle : IVehicle { public abstract void Run () ; public void Stop () { Console.WriteLine("Stopped!" ); } } class Car :Vehicle { public override void Run () { Console.WriteLine("Car is running..." ); } } class Truck : Vehicle { public override void Run () { Console.WriteLine("Truck is running..." ); } }
使用接口解耦
依赖关系
Car 依赖在 Engine 上,使得 Engine 变得不可替代。这样的紧耦合会使程序产生链式错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Engine { public int RPM { get ; private set ; } public void Work (int gas ) { RPM = 1000 * gas; } } class Car { private Engine _engine; public Car (Engine engine ) { _engine = engine; } public int Speed { get ; private set ; } public void Run (int gas ) { _engine.Work(gas); Speed = _engine.RPM / 100 ; } }
解耦
使用接口类型变量作为类的字段。当某一个类产生 bug,可让接口变量引用另一个无 bug 的类。
在下面的示例中,我们指明 IPhone 引用 NokiaPhone,但是我们也能十分容易让 IPhone 引用 EricssonPhone,形成了松耦合。
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 class PhoneUser { private IPhone _phone; public PhoneUser (IPhone phone ) { _phone = phone; } public void UsePhone () { _phone.Dail(); _phone.PickUp(); _phone.Send(); _phone.Receive(); } } interface IPhone { void Dail () ; void PickUp () ; void Send () ; void Receive () ; } class NokiaPhone : IPhone { public void Dail () { Console.WriteLine("Nokia calling..." ); } public void PickUp () { Console.WriteLine("Hello! This is Joe." ); } public void Receive () { Console.WriteLine("Nokia message ring..." ); } public void Send () { Console.WriteLine("hello world!" ); } } class EricssonPhone : IPhone { public void Dail () { Console.WriteLine("Ericsson calling..." ); } public void PickUp () { Console.WriteLine("Hello! This is Joe." ); } public void Receive () { Console.WriteLine("Nokia message ring..." ); } public void Send () { Console.WriteLine("hello world!" ); } }
接口隔离
接口隔离原则指客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。
一般方式
首先给出如下代码,大致意思为司机 Driver 调用 Dirve() 开车。客户端为 Driver,调用 Drive() 只会输出开 Vehicle 的状态。若客户想开 Tank,需要程序员打开 Driver 类,修改类中的代码,而且会多出不需要的方法 Fire(),这违反了接口隔离原则。
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 class Driver { private IVehicle _vehicle; public Driver (IVehicle vehicle ) { _vehicle = vehicle; } public void Drive () { _vehicle.Run(); } } interface IVehicle { void Run () ; } class Car : IVehicle { public void Run () { Console.WriteLine("Car is running." ); } } class Truch : IVehicle { public void Run () { Console.WriteLine("Truck is running." ); } } interface ITank { void Fire () ; void Run () ; } class LightTank : ITank { public void Fire () { Console.WriteLine("Boom!" ); } public void Run () { Console.WriteLine("Ka ka ka..." ); } } class MediumTank : ITank { public void Fire () { Console.WriteLine("Boom!!" ); } public void Run () { Console.WriteLine("Ka! Ka! Ka!" ); } } class HeavyTank : ITank { public void Fire () { Console.WriteLine("Boom!!!" ); } public void Run () { Console.WriteLine("Ka!! Ka!! Ka!!" ); } }
修改代码,让 ITank 继承两个接口,实现接口隔离。
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 class Driver { private IVehicle _vehicle; public Driver (IVehicle vehicle ) { _vehicle = vehicle; } public void Drive () { _vehicle.Run(); } } interface IVehicle { void Run () ; } interface IWeapon { void Fire () ; } class Car : IVehicle { public void Run () { Console.WriteLine("Car is running." ); } } class Truch : IVehicle { public void Run () { Console.WriteLine("Truck is running." ); } } interface ITank : IVehicle , IWeapon { }class LightTank : ITank { public void Fire () { Console.WriteLine("Boom!" ); } public void Run () { Console.WriteLine("Ka ka ka..." ); } } class MediumTank : ITank { public void Fire () { Console.WriteLine("Boom!!" ); } public void Run () { Console.WriteLine("Ka! Ka! Ka!" ); } } class HeavyTank : ITank { public void Fire () { Console.WriteLine("Boom!!!" ); } public void Run () { Console.WriteLine("Ka!! Ka!! Ka!!" ); } }
显示实现接口
在下面的代码中,显示实现接口 IKiller;只有在代码中用 IKiller 引用 WarmKiller 的实例,才能调用 Kill() 方法。显示实现接口同样能实现接口隔离,且更加简介、更加符合现实。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 interface IGentleman { void Love () ; } interface IKiller { void Kill () ; } class WarmKiller : IGentleman , IKiller { public void Love () { Console.WriteLine("I will love you forever." ); } void IKiller.Kill() { Console.WriteLine("Let me kill the enemy." ); } }
单元测试
接口降低了代码的耦合度,再结合单元测试来监控代码质量,能够使开发人员在检查出程序错误使较为方便地调式。
单元测试使用方法
我们首先要创建单元测试项目,比如 .Net Core 框架下的 xUnit 测试项目;接着在 [Fact]
标签下撰写测试方法,测试用 Assert 进行比对。
首先给出一段程序,大致意思为电源给电风扇供电,对于不同功率,电风扇工作时会返回不同状态。
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 namespace Main { public class Program { static void Main (string [] args ) { var fan = new DeskFan(new PowerSupply()); Console.WriteLine(fan.Work()); } } public interface IPowerSupply { int GetPower () ; } public class PowerSupply : IPowerSupply { public int GetPower () { return 130 ; } } public class DeskFan { private readonly IPowerSupply _powerSupply; public DeskFan (IPowerSupply powerSupply ) { _powerSupply = powerSupply; } public string Work () { int power = _powerSupply.GetPower(); if (power <= 0 ) { return "Won't work." ; } else if (power < 100 ) { return "Work slowly." ; } else if (power < 200 ) { return "Work fine." ; } else { return "Warning!" ; } } } }
在测试工程中添加测试方法。
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 namespace InterfaceExample.Tests { public class DeskFanTests { [Fact ] public void PowerLowerThanZero_OK () { var fan = new DeskFan(new PowerSupplyLowerThanZero()); var expected = "Won't work." ; var actual = fan.Work(); Assert.Equal(expected, actual); } [Fact ] public void PowerInFineRange_OK () { var fan = new DeskFan(new PowerSupplyHigherThan200()); var expected = "Warning" ; var actual = fan.Work(); Assert.Equal(expected, actual); } } class PowerSupplyLowerThanZero : IPowerSupply { public int GetPower () { return 0 ; } } class PowerSupplyHigherThan200 : IPowerSupply { public int GetPower () { return 322 ; } } }
打开测试资源管理器,运行所有测试,可看到通过和失败的测试。在失败的测试方法出加断点,可进入错误代码查看。
使用 Mock 简化单元测试代码
在 NuGet 程序包中安装 Moq,即可使用 Mock。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 namespace InterfaceExample.Tests { public class DeskFanTests { [Fact ] public void PowerLowerThanZero_OK () { var mock = new Mock<IPowerSupply>(); mock.Setup(ps => ps.GetPower()).Returns(() => 0 ); var fan = new DeskFan(mock.Object); var expected = "Won't work." ; var actual = fan.Work(); Assert.Equal(expected, actual); } [Fact ] public void PowerInFineRange_OK () { var mock = new Mock<IPowerSupply>(); mock.Setup(ps => ps.GetPower()).Returns(() => 263 ); var fan = new DeskFan(mock.Object); var expected = "Warning!" ; var actual = fan.Work(); Assert.Equal(expected, actual); } } }
反射
初识反射
反射是一种计算机处理方式:程序可以访问、检测和修改它本身状态或行为的这种能力,能提供封装程序集、类型的对象。
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 73 74 75 76 77 78 namespace Main { class Program { static void Main (string [] args ) { ITank tank = new HeavyTank(); var t = tank.GetType(); object o = Activator.CreateInstance(t); var fire = t.GetMethod("Fire" ); var run = t.GetMethod("Run" ); fire.Invoke(o, null ); run.Invoke(o, null ); } } class Driver { private IVehicle _vehicle; public Driver (IVehicle vehicle ) { _vehicle = vehicle; } public void Drive () { _vehicle.Run(); } } interface IVehicle { void Run () ; } interface IWeapon { void Fire () ; } class Car : IVehicle { public void Run () { Console.WriteLine("Car is running." ); } } class Truch : IVehicle { public void Run () { Console.WriteLine("Truck is running." ); } } interface ITank : IVehicle , IWeapon { } class LightTank : ITank { public void Fire () { Console.WriteLine("Boom!" ); } public void Run () { Console.WriteLine("Ka ka ka..." ); } } class MediumTank : ITank { public void Fire () { Console.WriteLine("Boom!!" ); } public void Run () { Console.WriteLine("Ka! Ka! Ka!" ); } } class HeavyTank : ITank { public void Fire () { Console.WriteLine("Boom!!!" ); } public void Run () { Console.WriteLine("Ka!! Ka!! Ka!!" ); } } }
反射的必要性
程序的逻辑有时候不能在编写代码时(静态)就完全确定,需要在用户与程序交互时(动态)确定。如果要求程序员在静态情况下枚举用户动态的操作,就有可能使得程序变得极其臃肿;甚至,我们都无法枚举所有用户的操作。这就需要反射,以不变应万变。
依赖注入
依赖注入指把有依赖关系的类放到容器中,解析出这些类的实例,其目的是实现类的解耦。
在下面的代码示例中,HeavyTank 类依赖于 ITank 接口,一般情况下,需要显示化 new 一个对象用 ITank 引用。采用依赖注入技术后,我们只需要在程序开始时向容器中注入与 ITank 绑定的类 HeavyTank,接下来就可以从容器中取出对应的实例即可。若原有代码多处有 ITank tank = new HeavyTank()
,此时想将 LightTank 与 ITank 绑定,需要修改多处代码;使用依赖注入的方式,只需要修改向容器注入时的一小部分代码即可。
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 namespace Main { public class Program { static void Main (string [] args ) { var sc = new ServiceCollection(); sc.AddScoped(typeof (ITank), typeof (HeavyTank)); var sp = sc.BuildServiceProvider(); ITank tank = sp.GetService<ITank>(); tank.Fire(); tank.Run(); } } interface IVehicle { void Run () ; } interface IWeapon { void Fire () ; } interface ITank : IVehicle , IWeapon { } class LightTank : ITank { public void Fire () { Console.WriteLine("Boom!" ); } public void Run () { Console.WriteLine("Ka ka ka..." ); } } class MediumTank : ITank { public void Fire () { Console.WriteLine("Boom!!" ); } public void Run () { Console.WriteLine("Ka! Ka! Ka!" ); } } class HeavyTank : ITank { public void Fire () { Console.WriteLine("Boom!!!" ); } public void Run () { Console.WriteLine("Ka!! Ka!! Ka!!" ); } } }
结合 SDK 进行插件开发
开发一款插件,插件用 DLL 形式保存。主体程序加载插件,插件中是各种动物叫声的资源。
主体程序的过程是:加载插件,拿到动物类,创建动物实例,并调用 Voice(int times) 方法。插件中有 Cat、Cow 等类,但是我们并不知道插件中有哪些资源,不可能直接调用 Cat.Voice(),因此反射的技术是必须要使用的。
首先创建一个类库项目作为 SDK:声明接口 IAnimal,声明类 UnfinishedAttribute。
1 2 3 4 5 6 7 8 namespace BabyStroller.SDK { public interface IAnimal { void Voice (int times ) ; } } namespace BabyStroller.SDK { public class UnfinishedAttribute : 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 namespace Animals.Lib1 { public class Cat : IAnimal { public void Voice (int times ) { for (int i = 0 ; i < times; i++) { Console.Write("喵" ); } Console.WriteLine("\n" ); } } } namespace Animals.Lib1 { class Sheep : IAnimal { public void Voice (int times ) { for (int i = 0 ; i < times; i++) { Console.Write("咩" ); } Console.WriteLine("\n" ); } } } namespace Animals.Lib2 { [Unfinished ] public class Cow : IAnimal { public void Voice (int times ) { for (int i = 0 ; i < times; i++) { Console.Write("哞" ); } Console.WriteLine("\n" ); } } } namespace Animals.Lib2 { public class Dog : IAnimal { public void Voice (int times ) { for (int i = 0 ; i < times; i++) { Console.Write("汪" ); } Console.WriteLine("\n" ); } } }
然后运行主程序。
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 namespace Main { class Program { static void Main (string [] args ) { var folder = Path.Combine(Environment.CurrentDirectory, "Animals" ); var files = Directory.GetFiles(folder); var animalTypes = new List<Type>(); foreach (var file in files) { var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(file); var types = assembly.GetTypes(); foreach (var t in types) { if (t.GetInterfaces().Contains(typeof (IAnimal)) && !t.GetCustomAttributes(false ).Any(a => a.GetType() == typeof (UnfinishedAttribute))) { animalTypes.Add(t); } } } while (true ) { for (int i = 0 ; i < animalTypes.Count; i++) { Console.WriteLine($"{i+1 } .{animalTypes[i].Name} " ); } Console.WriteLine("=================================" ); Console.WriteLine("Please choose animal." ); int index = int .Parse(Console.ReadLine()); if (index < 1 || index > animalTypes.Count) { Console.WriteLine("Input Error" ); Thread.Sleep(1500 ); Console.Clear(); continue ; } Console.WriteLine("Please set the time to shout." ); int times = int .Parse(Console.ReadLine()); var t = animalTypes[index - 1 ]; var o = Activator.CreateInstance(t); var animal = o as IAnimal; animal.Voice(times); Thread.Sleep(1500 ); Console.Clear(); } } } }
泛型
泛型存在的必要性
泛型的存在可避免成员膨胀或类型膨胀。
泛型与类、接口、委托等都存在正交性。
泛型可与委托、Lambda 表达式结合,形成简洁优美的代码。
泛型类
用尖括号声明一个未知类型的名称。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 namespace Main { class Program { static void Main (string [] args ) { Box<Apple> box = new Box<Apple>() { Cargo = new Apple() { Color = "Red" } }; Console.WriteLine(box.Cargo.Color); } } class Apple { public string Color { get ; set ; } } class Book { public string Name { get ; set ; } } class Box <TCargo > { public TCargo Cargo { get ; set ; } } }
泛型接口
泛型类和具体类都能实现泛型接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 namespace Main { class Program { static void Main (string [] args ) { Student<int > student = new Student<int >() { ID = 101 , Name = "Joe" }; } } interface IUnique <TId > { TId ID { get ; set ; } } class Student <TId > : IUnique <TId > { public TId ID { get ; set ; } public string Name { get ; set ; } } }
泛型方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 namespace Main { class Program { static void Main (string [] args ) { int [] a = { 1 , 2 , 3 , 4 , 5 }; int [] b = { 6 , 7 , 8 , 9 , 10 }; Console.WriteLine(string .Join(',' , Merge(a, b))); } static T[] Merge <T >(T[] a, T[] b ) { T[] c = new T[a.Length + b.Length]; for (int i = 0 ; i < a.Length; i++) { c[i] = a[i]; } for (int i = a.Length; i < a.Length + b.Length; i++) { c[i] = b[i - a.Length]; } return c; } } }
.Net Core 中的泛型类
1 2 3 List<int > list = new List<int >(); Dictionary<string , int > keyValuePairs = new Dictionary<string , int >();
partial 类
简介
C# 允许 partial 类的成员定义拆分到多个源文件中。每个源文件包含类定义的一部分,编译应用程序时把所有部分组合起来。
更神奇的是,partial 类的不同部分还能用不同语言编写。
举例
在 Book1.cs 中定义属性 Name。
Book2.cs 中再定义属性 Name 报错。