C#语法基础总结(超级全面)(二)

文章目录

c#语法

基本元素

小扳手是属性,小方块是方法,小闪电是事件

关键字

操作符(operator)

typeof 查看类型的内部结构
defult 返回内存块全刷成0的值
new(关键字) 创建一个类型的实例
var(关键字) 声明隐式类型的变量
checked 检查这个值有没有溢出
sizeof 获取基本数据类型所占字节数
explicit 显示类型转换
implicit 隐式类型转换
csharp 复制代码
int x=defult(int)
csharp 复制代码
uint y=checked(x+1) 

??用于判断一个变量在为null时,返回双问号后面的一个指定的值。

类型转换

隐式转换:不丢失精度的转换、子类向父类的转换(int转long等)、装箱

显式转换:可能丢失精度的转换、拆箱、使用convert类、ToString方法与个数据类型的Parse的转换

csharp 复制代码
class program
{
    static void Main(string[] args)
    {
        Stone stone=new Stone();
        stone.Age=5000;
        Monkey wukongSun =(Monkey)stone;
        Console.WriteLine(wukongSun.Age);
    }
}
class Stone
{
    public int Age;
    public static explicit operator Monkey(Stone stone)
    {
    Monkey m=new Monkey(); 
    m.Age=stone.Age/500; //将石头转换为猴子,石头500岁为猴子1岁
    return m;
    } //显示类型转换就是一个目标类型的实例构造器,这个构造器写在被转换的这个数据类型里
}
class Monkey
{
    public int Age;
}

标识符(Identifier)

大小写规范(变量用驼峰法、类名和名称空间用帕斯卡命名法(单词首字母大写))

语句

方法体里面才有语句。

表达式语句、声明语句(int x)、嵌入式语句(在 if 判断后面嵌入的一条语句)、选择语句(if、switch) 、标签语句(在一条语句前面加上标识符)、块语句(用花括号括起来的)

try语句

try语句提供一种机制,用于捕捉在块的执行期间发生的各种异常,catch语句会执行错误时的代码。

csharp 复制代码
class Calculator
{
    public int Add(string a1, string a2)
    {
        int a = 0, b = 0;
        try
        {
            a = int.Parse(a1); //把string转化int
            b = int.Parse(a2); 
        }
        catch
        {
            Console.WriteLine("错误");
        }

        int result = a + b;
        return result;
    }
}

catch(ArgumentException ane)
{
    Console.WriteLine(ane.Message);
}//catch里可以加入ArgumentException参数,如果有ArgumentException异常会指出出现错误的类型,此外还有FormatException异常等

迭代语句(循环语句)

while、do-while、for、foreach

foreach语句用于枚举一个集合的元素,并对该集合中的每个元素执行一次相关的嵌入式语句(类似python中的for)。

csharp 复制代码
int[] intArray = new int[] { 2, 2, 3, 4, 5, 6, 7, 8 };
foreach (int i in intArray)
{
    //i是迭代变量,代表数组的元素
    Console.WriteLine(i);
}

实现了IEnumerable这个接口的类就是可以遍历的集合,能够被遍历的集合都可以获得自己的迭代器。数组属于arry这个类。

csharp 复制代码
int[] intArray = new int[] { 2, 2, 3, 4, 5, 6, 7, 8 };
IEnumerator<int> enumerator = intArray.AsEnumerable().GetEnumerator();
//获取数组的迭代器
while (enumerator.MoveNext())
{
    Console.WriteLine(enumerator.Current);
} //会打印出数组元素

索引器

它使对象能够用与数组相同的方式(即使用下标)进行索引。

文本(字面值)

整数、实数、字符、字符串、布尔、空

csharp 复制代码
float x=3.0F
double y=4.0D
long z=3L
 dynamic myVar=100 //动态类型

五大数据类型

由object派生出两大类型,所有类型都继承object。

引用类型:

引用变量存储的是实例(数据)在堆内存上的内存地址

类(class):windows、Form、Console、String

字段(field ):为一个对象或者类存储数据。也叫成员变量。只读字段只能进行初始化,不能再被赋值。

csharp 复制代码
class Student
{
    public int Age; //字段
    public int sg;
    public chus() //构造器
    {
        Student ss=new Student();
        ss.Age = 1;
    }
}

接口(Interfaces)

委托(Delegates)

委托就是将方法作为参数(模仿函数指针)

值类型:

值类型的数据会直接保存在变量的内存空间中

结构体(Structures):int32、int64、Single、Double、long,用struct声明

枚举(Enumerations):用enum声明

值类型存储在内存的栈里,引用类型存储在堆里。值类型没有实例,值类型的实例和变量是合二为一的。

变量、对象与内存

变量就是用来存储数据,以变量名所对应的内存地址为起点,以其数据类型所要求的存储空间为长度的一块内存区域。变量名表示变量的值在内存中的位置,每个变量都有一个类型以决定什么样的值能够存入变量。

变量一共有7种:静态变量、实例变量(成员变量、字段)、数组元素、值参数、引用参数、输出形参、局部变量。简单的讲,局部变量就是方法体里声明的变量,在栈上分配内存。

参数只有三种:值参数、引用参数、输出参数

成员变量声明后不赋值,默认值为把内存块都刷成0

装箱和拆箱

装箱:object类型变量obj所要引用的一个值,它不是堆上的实例,而是栈上的值类型的值,obj会先把这个值类型的值copy一下,然后在堆上找一块可以存储的空间,然后把这块空间变成对象,将这块地址存入到obj这个变量中。(把值类型数据从栈搬到堆)(二级指针?)

拆箱:获得object类型变量引用的值类型的值,存储到栈上。

可以强转获得:

csharp 复制代码
int x=100;
object obj;
obj=x;  //装箱
int y=(int)obj;  //拆箱

装箱和拆箱从堆里往栈里搬东西,所以会损失性能

类本身是抽象数据和抽象行为的载体。

类的修饰符:new、public、protected、internal、private、abstract、sealed、static

继承关系分组:abstract(必须继承)、sealed(禁止继承)

访问级别:public、internal

未嵌套在其他类型中的类的默认可访问性为internal,同一名称空间下的类可以访问;public公有的,其他项目也可访问这个类;
public所有地方都可以访问到这个成员;internal此命名空间类都可访问,别的项目无法访问;类成员的默认可访问性为private,仅供此类访问。

类成员的访问级别不能超过类,以类的访问级别为上限。

类的实例化

csharp 复制代码
new 类名();

引用变量

引用变量可以引用类的实例

csharp 复制代码
类名 引用变量名= new 类名()
Form myform = new Form();

this代表当前类的实例

静态类中不能声明实例成员

类的三大成员(属性、方法、事件)

属性(property)

存储数据,组合起来表示类或对象当前的状态(成员变量),是一种用于访问对象或类型的特征 的成员,特征反映了状态。例如豆腐的温度,高温这个特征反映了豆腐现在的状态,所以通过属性反映类型的状态。

属性是字段的自然扩展,由字段发展而来,字段更偏向于实例对象在内存中的布局,property更偏向于反映现实世界对象的特征。语法糖,把背后复杂的机制隐藏了起来。

csharp 复制代码
internal class Program
{
    static void Main(string[] args)
    {
        Calculator c = new Calculator();
        Student stu1=new Student();
        Student stu2 =new Student();
        Student stu3 =new Student();
        stu1.SetAge(20);
        stu2.SetAge(20);
        stu3.SetAge(20);
        int avgAge=(stu1.GetAge()+stu2.GetAge()+stu3.GetAge())/3;
        Console.WriteLine(avgAge);
    }
}
class Student
{ 
    private int Age; //设置为私有的,
    public int GetAge() //获取这个字段的值
    {
        return this.Age;
    }
    public void SetAge(int value) //对我们设置的值进行约束
    {
        if (value >= 0 && value <= 120)
        {
            this.Age= value;   
        }
        else
        {
            throw new Exception("Age value has error"); //出现错误程序终止运行
        }
    }
}

类里面的属性应该这样写:

csharp 复制代码
private int age; //字段
public int Age //属性
{
    get //获取这个属性的值
    {
        return this.age;
    }
    set //约束属性值
    {
        if (value >= 0 && value <= 120)
        {
            this.age = value;
        }
        else
        {
            throw new Exception("age error");
        }
    }
}
//如果一个属性只有get没有set说明是只读属性

建议用属性去获取字段的值,字段最好是private或protected,打出propfull按两下tap键,会生成属性的完整声明。

方法(函数)

表示类或对象能做什么

当一个函数以类的成员的身份出现时, 就叫做方法。方法永远都是类或结构体的成员。

方法参数
值参数

值参数:声明是不带修饰符的形参。值形参会创建新的存储位置。

引用类型的变量存储的值就是我们引用类型实例在堆内存当中的地址。引用类型的传值参数传递的是实例的地址(类似指针),但是在方法里去改变这个参数的值,会改变方法外部引用实例的值,但如果在方法里重新实例化(创建一个新的对象)就不会改变(因为重新实例化后,指向的不是同一块地址)。

引用参数

引用形参是用ref(像&和*符号)修饰符声明的形参,与值形参不同,引用形参不回创建新的存储位置。
值类型引用参数直接指向实际参数的内存地址。变量在作为引用形参前,必须先明确赋值。(一级指针形参)

引用类型的引用参数,传参传进来的是一个引用类型的变量存储的地址(这个地址存储的是对象在堆内存中的地址),而我们的引用参数存储的是引用类型变量所指向的的地址(实例的地址),他们指向的内存地址就是同一个地址。如果在方法内部创建新对象,外部变量也会改变。(类似二级指针,但不完全一样)

输出参数

用out修饰符声明的形参是输出形参。类似于引用形参,输出形参不创建新的存储位置。输出形参表示的存储位置恰是在该方法调用中作为实参给出的那个变量所表示的存储位置,它和传进来的实参指向的是同一个内存位置。不要求输出参数在传入方法前要明确的赋值(因为你是通过这个参数向外输出,原来赋的值就会被丢弃)

值类型输出参数方法体内必须有对输出变量的赋值操作,输出参数并不创建变量的副本。

csharp 复制代码
nternal class Program
    {
        static void Main(string[] args)
        {
            double x;
            bool b = Student.TryParse("789", out x);
            if (b == true)
            {
                Console.WriteLine(x + 1);
            }
        }
    }
    class Student
    {
        public string Name;
        public static bool TryParse(string input,out double result) //返回值是bool类型输出参数为double类型
        {
            try
            {
                result = double.Parse(input);
                return true; //没有异常返回true,并赋值
            }
            catch 
            {
                result = 0;
                return false;
            }
        }
    }

引用类型输出参数,如果在方法体内去新创建一个实例,那么我们的输出参数和我们的引用变量指向的是同一个地址,这个地址所存储的就是我们新创建的对象在堆内的地址。

数组参数

数组参数在声明时用params关键字修饰(只能有一个参数由params修饰,并且是形参列表中的最后一个)

具名参数

具名参数严格意义上并不是参数的种类,而是参数的使用方法。

csharp 复制代码
static void Main(string[] args)
{
    opop(name: "tim", age: 34);
}
static void opop(string name,int age)
{
    Console.WriteLine("name:{0},agg:{1}",name,age); 
}

优点:提高代码可读性,不受参数列表顺序约束

可选参数

参数因为具有默认值而变得"可选",不推荐使用可选参数

csharp 复制代码
static void Main(string[] args)
{
    opop();
}
static void opop(string name="tim",int age=23)
{ //给参数设置默认值
    Console.WriteLine("name:{0},agg:{1}",name,age); 
}
扩展方法(this参数)

方法必须是公有的、静态的被public static所修饰,this参数必须是参数列表中的第一个,由this修饰。而且必须放在一个静态类中。

当我们无法对一个类型的源码进行修改的时候,可以使用扩展方法为目标数据类型来追加方法。

csharp 复制代码
nternal class Program
    {
        static void Main(string[] args)
        {
            double x = 3.14159;
            double y = x.Round(4);
            //Round本来是double里的函数,扩展之后可以直接调用,x作为第一个参数
            Console.WriteLine(y);
        }
    }
    static class Student //创建一个静态类,构建方法
    {
       public static double Round(this double input,int digits) //第一个参数前面加上this
        {
            double result=Math.Round(input,digits);
            return result;  
        }  
    }
方法的重载

方法的名字可以相同,但是方法的签名(代表唯一性)不能够一样。

方法签名 :由方法的名称、类型形参的个数和它每一个形参的类型和种类(值、引用或输出)组成。方法签名不包含返回类型

csharp 复制代码
public int Add(ref int a,int b)
//加上ref变成传引用的参数 
    
public int Add(out int a,int b)
//加上out变成传输出的参数 

可以改变参数类型或者种类、或者增加参数数量构成重载

构造器(constructor)

构造器是类型(值类型、引用类型)的成员之一,构造自己的内存块(对实例进行初始化),没有返回值,不需要写方法返回类型,构造器=构造函数、成员函数=方法。静态构造器只能构造静态成员,无法构造实例成员。

csharp 复制代码
Student stu = new Student(); //调用构造器
//如果没有写构造器,编译器会自动生成一个默认的构造器,会对类里面的字段进行初始化
csharp 复制代码
public Student(int initld,string initName)
{   //带参数的构造器
    this.ID = initld; //this指当前类
    this.Name = initName;
}
Student stu = new Student(2,"opop");
//类的实例化也要带上参数

打出ctor,按两下tap键会自动生成一个构造器

析构器

当我们对象在内存当中没有任何变量在引用它的时候,这个对象就不能够再被访问了,这块内存为垃圾内存,就需要回收(例如实例化了一个对象,程序运行结束后,会回收实例的内存,此时就会执行析构函数)。在回收过程中如果我们想让它做些事情,就放在析构器里。

csharp 复制代码
~类名(){
    
}

委托

委托是函数指针的"升级版",可以把函数作为方法的参数。直接调用:通过函数名调用,间接调用:通过函数指针调用。

Action委托
csharp 复制代码
internal class Program
{
    static void Main(string[] args)
    {
        Student stu=new Student();
        Action action = new Action(stu.Report);
        //获得方法名
        stu.Report(); //直接调用
        action.Invoke(); //间接调用
        action();
    }
}
class Student
{
   public void Report()
    {
        Console.WriteLine("111");
    }
    public int Add(int a,int b)
    {
        int result = a + b;
        return result;
    }
    public int Sub(int a,int b)
    {
        int result = a - b;
        return result;  
    }
}
Function类型委托
csharp 复制代码
static void Main(string[] args)
{
    Student stu=new Student();
    Action action = new Action(stu.Report);
    Func<int, int, int> func1 = new Func<int, int, int>(stu.Add);
    Func<int, int, int> func2 = new Func<int, int, int>(stu.Sub);
    //前面两个是参数类型,后面是返回值类型,括号里面是方法
    int x = 100;
    int y = 200;
    int z = 0;
    z = func1.Invoke(x, y); //间接调用函数,也可以省略Invoke(仿照函数指针格式)
    Console.WriteLine(z);
    z= func2.Invoke(x, y);
    Console.WriteLine(z);
}
委托的声明(自定义委托)

委托是一种类,类是数据类型所以委托也是一种数据类型。声明方式与一般类不同,委托与所封装的方法必须"类型兼容"(参数类型和返回值类型)。

csharp 复制代码
public delegate double Calc(double x,double y);
//delegate声明委托关键字,后面是目标方法的返回值类型,委托名,目标方法参数
csharp 复制代码
 Student stu = new Student();
 Calc calc1=new Calc(stu.Add); //传递给委托一个相同类型参数的方法
 Calc calc2 = new Calc(stu.Sub);
 Calc calc3 = new Calc(stu.Mul);
 Calc calc4 = new Calc(stu.Div);
double a=100,b=100;
c = calc1(a,b); //间接调用,传递参数
委托的一般使用

(1)把方法当作参数传给另一个方法:模板方法、回调(callback)方法调用指定的外部方法。

当以回调方法来使用委托时,把委托类型的参数传进主调方法里面去,被传进主调方法里面的委托类型参数内部,会封装一个被回调的方法。常位于代码末尾,一般没有返回值

缺点:(1)这是一种方法级别的紧耦合(2)使可读性下降,debug难度增加(3)委托回调、异步调用和多线程纠缠在一起,会使代码难以阅读和维护(4)使用不当可能造成内存泄漏和程序性能下降

事件

事件的应用

事件:使类或对象具备通知能力的成员。类或对象通知其他类或对象的机制,为c#特有,用于对象或类间的动作协调与信息传递。例如响铃这个事件,让手机具备了通知关注者的能力。

消息(事件参数):由事件发送出来的与事件本身相关的数据。

响应事件(处理事件):根据通知和时间参数来采取行动的行为。

事件处理器:处理时间时具体所做的的事情。

事件的功能=通知+可选的事件参数

1.事件的拥有者:对象

2.事件成员:事件只能出现在+=或-=左边(要么为事件添加事件处理器,要么移除处理器)

3.事件的响应者:当一个事件发生时,被通知的对象或类。它们会使用自己所拥有的事件处理器,来根据业务逻辑处理这个发生了的事件。

4.事件的处理器:本质上是一个回调方法。

5.事件订阅:当一个事件发生时,事件的拥有者都会通知谁(被通知的对象一定是订阅了这个事件)。把事件处理器与事件关联在一起,拿什么样的方法(事件处理器)才能够处理这个事件。例如,孩子迷路了,孩子能够告诉你一个地理信息(当前所在地方的路名等信息),那么这个时候你需要准备的事件处理器,就必须是一个能够处理地理信息的方法。所以用于订阅事件的事件处理器必须和事件遵守同一个约定(既约束了事件能够把什么样的消息发送给事件处理器,也约束了事件处理器能够处理什么样的消息)。如果事件是使用某个约定定义的,而且事件处理器也遵循同样的约定,我们就说事件处理器与事件是匹配的,就可以拿这个处理器去订阅这个事件。(约定可以理解为委托的参数,不一样类型的参数或者不同数量的参数可以构成不同的方法以供回调)

csharp 复制代码
internal class Program
{
    static void Main(string[] args)
    {
        Timer timer = new Timer(); //事件拥有者
        timer.Interval = 1000;
        Boy boy = new Boy(); //事件响应者
        timer.Elapsed += boy.Action; //事件订阅,事件订阅操作符是+=,左边是事件,右边是处理器
        timer.Start();
        Console.ReadLine();
    }
}
class Boy
{
    internal void Action(object sender, ElapsedEventArgs e) //事件处理器
    {
        Console.WriteLine("jump!");
    }
}
csharp 复制代码
internal class Program
{
    static void Main(string[] args)
    {
        Form form = new Form(); //事件的拥有者(Form类)
        Con con = new Con(form); //事件响应者(Con类)
        //事件的处理结果是作用在事件拥有者身上的(响应函数传的是拥有者参数)
        form.ShowDialog();
    }
}
class Con
{
    private Form form;
    public Con(Form form) //构造函数,传引用类型
    {
        if(form != null)
        {
            this.form = form; //让实例字段指向之前new出来的Form类型实例
            //事件是form.Click
            this.form.Click += this.FormClicked;//事件订阅,选择这个时间的响应处理器
        }
    }
    private void FormClicked(object sender, EventArgs e) //事件处理器
    {
        this.form.Text = DateTime.Now.ToString();
    }
}
csharp 复制代码
internal class Program
{
    static void Main(string[] args)
    {
        myForm form = new myForm(); //事件拥有者同时也是事件的响应者
        form.Click+=form.Formclick; //事件:click,事件的订阅
        form.ShowDialog();
    }
}
class myForm : Form //继承
{
    internal void Formclick(object sender, EventArgs e) //事件处理器
    {
        this.Text = DateTime.Now.ToString();
    }
}
csharp 复制代码
internal class Program
{
    static void Main(string[] args)
    {
        myForm form = new myForm();
        form.ShowDialog();
    }
}
class myForm : Form //继承
{
    private TextBox textBox;
    private Button button; //事件的拥有者
    public myForm()
    {
        this.textBox = new TextBox();
        this.button = new Button();
        this.Controls.Add(this.textBox);
        this.Controls.Add(this.button);
        this.button.Click += this.ButtonClicked; //事件订阅
        //事件:click,
        //事件的响应者是我们这个myForm对象,因为ButtonClick这个处理器是myForm类型
        //的实例方法
    }

    private void ButtonClicked(object sender, EventArgs e) //事件处理器
    {
        this.textBox.Text = "Hello Wrold!!!!!!!!!!!";
    }
}
自定义事件
复制代码
public event 委托名 事件名 //简要声明
//public能够被外界访问到,evnt声明事件关键字,约束这个事件的委托类型,声明事件的名字
csharp 复制代码
//最好打断点看看过程
using System;
using System.Threading;
namespace EventExample
{
    class Program
    {
        static void Main(string[] args)
        {
            // 1.事件拥有者
            //触发这个事件一定是事件拥有着内部逻辑触发的这个事件
            var customer = new Customer();
            // 2.事件响应者
            var waiter = new Waiter();
            // 3.Order 事件成员 5. +=事件订阅
            customer.Order += waiter.Action;
            customer.Action();
            customer.PayTheBill();
        }
    }
    // 该类用于传递点的是什么菜,作为事件参数,需要以 EventArgs 结尾,且继承自 EventArgs
    public class OrderEventArgs:EventArgs
    {
        public string DishName { get; set; }
        public string Size { get; set; }
    }
    // 声明一个委托类型,因为该委托用于事件处理,所以以 EventHandler 结尾
    // 注意委托类型的声明和类声明是平级的
    public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
    public class Customer
    {
        // 委托类型字段
        private OrderEventHandler orderEventHandler;
        // 完整的事件声明
        public event OrderEventHandler Order
        {
            add { this.orderEventHandler += value; }
            remove { this.orderEventHandler -= value; }
        }
        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);
            }
            if (this.orderEventHandler != null) //触发这个事件,判断这个委托是否为空
            {
                var e = new OrderEventArgs();
                e.DishName = "Kongpao Chicken";
                e.Size = "large";
                this.orderEventHandler.Invoke(this,e); //使用委托字段,间接调用
            }
        }
        public void Action()
        {
            Console.ReadLine();
            this.WalkIn();
            this.SitDown();
            this.Think();
        }
    }
    public class Waiter
    {
        // 4.事件处理器
        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;
        }
    }
}

事件的本质就是委托字段的一个包装器,这个包装器对委托字段的访问起限制作用,谨防"借刀杀人"。加了event关键字相当于允许存在两个相同名称的变量,一个是对外public的order事件,一个是对外private的order委托。我们可以把委托的参数列表看作是事件发生后发送给事件响应者的"事件消息"。事件基于委托,处理器需要啥,事件就传递啥(决定了参数列表),同时处理器是一个方法,能存储方法及其参数类型的只有委托。

父类(基类)转子类

csharp 复制代码
public void Action(object sender, EventArgs e) //任何类型都是object的子类,所以都可以被传参
{
    //as表示转化为
    Customer customer = sender as Customer; //向下转换(父类转子类),因为objct基类访问不了子类属性
    OrderEventArgs order = e as OrderEventArgs;
    Console.WriteLine("I will serve you the dish - {0}.", order.DishName); //转化类型后,可以访问DishName
    double price = 10;
   
    customer.Bill += price;
}
事件的命名
事件与委托的关系
  • 事件真的是"以特殊方式声明的委托字段/实例"吗?

    • 不是!只是声明的时候"看起来像"(对比委托字段与事件的简化声明,field-like)
    • 事件声明的时候使用了委托类型,简化声明造成事件看上去像一个委托的字段(实例),而 event 关键字则更像是一个修饰符 ------ 这就是错觉的来源之一
    • 订阅事件的时候 += 操作符后面可以是一个委托实例,这与委托实例的赋值方法语句相同,这也让事件看起来像是一个委托字段 ------ 这是错觉的又一来源
    • 重申:事件的本质是加装在委托字段上的一个"蒙版"(mask),是个起掩蔽作用的包装器。这个用于阻挡非法操作的"蒙版"绝不是委托字段本身
  • 为什么要使用委托类型来声明事件?

    • 站在 source 的角度来看,是为了表明 source 能对外传递哪些消息
    • 站在 subscriber 的角度来看,它是一种约定,是为了约束能够使用什么样签名的方法来处理(响应)事件
    • 委托类型的实例将用于存储(引用)事件处理器
  • 对比事件与属性

    • 属性不是字段 ------ 很多时候属性是字段的包装器,这个包装器用来保护字段不被滥用
    • 事件不是委托字段 ------ 它是委托字段的包装器,这个包装器用来保护委托字段不被滥用
    • 包装器永远都不可能是被包装的东西

在上图中是被迫使用事件去做 !=.Invoke(),学过事件完整声明格式,就知道事件做不了这些。在这里能这样是因为简略格式下事件背后的委托字段是编译器自动生成的,这里访问不了。

总结:事件不是委托类型字段(无论看起来多像),它是委托类型字段的包装器,限制器,从外界只能访问 += -= 操作。

静态成员与实例成员

静态(static)成员在语义上表示"类的成员",类与生俱来的。

实例(非静态)成员在语义上表示"对象成员",属于对象的而不是某个类。例如对于人这个类,它的静态属性为平均身高、平均体重;而对于某个人(实例)的属性应该为身高、体重。

绑定(Binding)指的是编译器如何把一个成员与类或对象关联起来

如果类的成员函数是静态的可以通过类名直接调用该函数,如果不是静态则需要类的实例化对象才能用

类的继承

继承就是完整接收父类成员的前提下(字段、属性、方法、事件),对父类进行横向(类成员变多)与纵向(不增加类成员个数,对某些类成员的版本进行扩充或者是对类成员的重写)的扩展。

基类--派生类、父类--子类

一个派生类的实例,也是基类的实例;基类的实例不一定是派生类的实例。那么我们可以用一个父类类型的变量去引用子类类型的实例。派生类的访问级别不能超过基类。

csharp 复制代码
Vehicle vehicle =new Car();

子类会全部继承父类的成员。一旦一个类成员被加入继承链中,就不能把它去掉了。

public公有的,其他项目也可访问这个类;未嵌套在其他类型中的类的默认可访问性为internal,同一名称空间下的类可以访问;
public其他地方都可以访问到这个成员;internal此命名空间类都可访问,别的项目无法访问;类成员的默认可访问性为private,仅供此类访问。

csharp 复制代码
base可以访问基类对象

在子类中修改和父类共有的属性,会修改父类属性的值(重写)。

csharp 复制代码
class Vehicle
{
    public Vehicle(string owner)
    {
        this.Owner=owner;
    }
    public string Owner { get; set; }
}
class Car:Vehicle
{
    //当我们Car的构造器被调用时,那么它会默认调用父类的无参构造器
    //如果父类构造器有参数,那么我们子类构造器要给出参数
    public Car(string owner) : base(owner)
    {
        //因为在父类构造器里已经把Owner的值设置为owner了,没必要在设置一遍了。
    }
}

当父类的成员被protected修饰时,它的子类都可以访问这个成员,而不在这个继承链上的其他类型无法访问这个成员。

重写与多态

不同的子类可能有不同的行为,所以要进行重写。

子类对父类成员的重写。因为类成员个数还是那么多,只是更新版本,所以又称为纵向扩展。注:重写时,Car 里面只有一个版本的 Run。

重写需要父类成员标记为 virtual ,子类成员标记 override。注:被标记为 override 的成员,隐含也是 virtual 的,可以继续被重写。

virtual:可被重写的、名义上的、名存实亡的

csharp 复制代码
class Program
{
    static void Main(string[] args)
    {
        var car = new Car();
        car.Run();
        // Car is running!
        var v = new Vehicle();
        v.Run();
        // I'm running!
    }
}
class Vehicle
{
    public virtual void Run()
    {
        Console.WriteLine("I'm running!");
    }
}
class Car : Vehicle
{
    public override void Run()
    {
        Console.WriteLine("Car is running!");
    }
}
Hide

如果子类和父类中函数成员签名相同,但又没标记 virtual 和 override,称为 hide 隐藏。

这会导致 Car 类里面有两个 Run 方法,一个是从 Vehicle 继承的 base.Run(),一个是自己声明的 this.Run()。

csharp 复制代码
Vehicle v=new Car;

可以理解为 v 作为 Vehicle 类型,它本来应该顺着继承链往下(一直到 Car)找 Run 的具体实现,但由于 Car 没有 Override,所以它找不下去(找不到最新的版本),只能调用 Vehicle 里面的 Run(打印出来的是 Im running!)。

注:

  1. 新手不必过于纠结 Override 和 Hide 的区分、关联。因为原则上是不推荐用 Hide 的。很多时候甚至会视 Hide 为一种错误
  2. Java 里面是天然重写,不必加 virtual 和 override,也没有 Hide 这种情况
  3. Java 里面的 @Override(annotation)只起到辅助检查重写是否有效的功能
Polymorphism 多态

多态:C# 支持用父类类型的变量引用子类类型的实例。函数成员的具体行为(版本)由对象决定。

回顾:因为 C# 语言的变量和对象都是有类型的,就导致存在变量类型与对象类型不一致的情况,所以会有"代差"。

csharp 复制代码
class Program
{
    static void Main(string[] args)
    {
        Vehicle v = new RaceCar();
        v.Run();
        // Race car is running!
        Car c = new RaceCar();
        c.Run();
        // Race car is running!
        Console.ReadKey();
    }
}
class Vehicle
{
    public virtual void Run()
    {
        Console.WriteLine("I'm running!");
    }
}
class Car : Vehicle
{
    public override void Run()
    {
        Console.WriteLine("Car is running!");
    }
}
class RaceCar : Car
{
    public override void Run()
    {
        Console.WriteLine("Race car is running!");
    }
}
csharp 复制代码
class Program
{
    static void Main(string[] args)
    {
        Vehicle v = new Car();
        v.Run(); 
        //Car is running!
    }
}
class Vehicle
{
    public virtual void Run() //如果不重写就会调用这个方法
    {
        Console.WriteLine("I'm running!");
    }
}
class Car : Vehicle
{
    public override void Run()
    {
        Console.WriteLine("Car is running!");
    }
}

接口,抽象类,SOLID,单元测试

SOLID

  • SRP:Single Responsibility Principle

  • OCP:Open Closed Principle

  • LSP:Liskov Substitution Principle

  • ISP:InterfaceSegregation Principle

  • DIP:Dependency Inversion Principle

SOLID(单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)是由罗伯特·C·马丁在21世纪早期引入的记忆术首字母缩略字,指代了面向对象编程和面向对象设计的五个基本原则。

首字母 指代 概念
S 单一功能原则 对象应该仅具有一种单一功能。
O 开闭原则 软件体应该是对于扩展开放的,但是对于修改封闭的。
L 里氏替换原则 程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换。参考 契约式设计
I 接口隔离原则 多个特定客户端接口要好于一个宽泛用途的接口。
D 依赖反转原则 一个方法应该遵从"依赖于抽象而不是一个实例"。依赖注入是该原则的一种实现方式。

抽象类与开闭原则

抽象类指的是函数成员至少有一个没有被完全实现的类,一但类里有抽象方法,这个类就变成了抽象类。

csharp 复制代码
abstract class Student //抽象类
{
    abstract public void Study(); //抽象方法,方法没有被完全实现,没有方法体,用abstract修饰
    //抽象方法不能是private,可以是public,internal,因为最后肯定是这个抽象类的子类去实现这个方法
    //接口要求成员方法必须是public
}

因为抽象类有未被实现的方法,没有具体的行为,所以不允许实例化一个抽象类(如果去调用这个方法的话,计算机不知道怎么执行)。

一个类不允许实例化,它就只剩两个用处了:

  1. 作为基类,在派生类里面实现基类中的 abstract 成员

  2. 声明基类(抽象类)类型变量去引用子类(已实现基类中的 abstract 成员)类型的实例,这又称为多态

所以我们应该封装那些不变的、稳定的、固定的和确定的成员,而把那些不确定的、有可能改变的的成员声明为抽象成员,并且留给子类实现。

csharp 复制代码
class Program
{
    static void Main(string[] args)
    {
        Vehicle v = new Car(); //多态
        v.Run();
    }
}
abstract class Vehicle
{
    public abstract void Run();
}
class Car : Vehicle
{
    public override void Run() //对这个抽象类进行重写,如果你不重写,那你也要是抽象类
    {
        Console.WriteLine("Car is running!"); //所以抽象类专为基类而生的
    }
}

接口

纯抽象类(纯虚类)==接口(所有成员都是抽象的,没有实现)。

接口和抽象类都是"软件工程产物",如果不是为了修复 bug 和添加新功能,别总去修改类的代码,特别是类当中函数成员的代码。防止违反开闭原则。接口为解耦而生:"高内聚,低耦合",方便单元测试。

csharp 复制代码
class Program
{
    static void Main(string[] args)
    {
        Vehicle v = new Car();
        v.Run();
    }
}
abstract class VehicleBase //纯抽象类(接口)
{
    abstract public void Run();
    abstract public void Fill();
    abstract public void Stop();
}
abstract class Vehicle:VehicleBase
{
    public override void Fill()
    {
        Console.WriteLine("Fill");
    }
    public override void Stop()
    {
        Console.WriteLine("Stop");
    }
}
class Car : Vehicle
{
    public override void Run()
    {
        Console.WriteLine("Car is running!");
    }
}
csharp 复制代码
interface IVehicle //接口可以这样写
{
    void Run(); 
    void Fill(); //接口要求成员必须是public,并且接口所有成员都是abstract,所以省去避免重复
    void Stop();
}
abstract class Vehicle:IVehicle //不完全实现的抽象类
{
    public void Fill() //去掉override,因为上面去掉了abstract
    {
        Console.WriteLine("Fill");
    }
    public void Stop()
    {
        Console.WriteLine("Stop");
    }
    abstract public void Run();
}
class Car : Vehicle //根据不完全实现的基类去创建具体类
{
    public override void Run()
    {
        Console.WriteLine("Car is running!");
    }
}

依赖与耦合

现实世界中有分工、合作,面向对象是对现实世界的抽象,它也有分工、合作。

类与类、对象与对象间的分工、合作。

在面向对象中,合作有个专业术语"依赖",依赖的同时就出现了耦合。依赖越直接,耦合就越紧。

Car 与 Engine 紧耦合的示例:

csharp 复制代码
class Program
{
    static void Main(string[] args)
    {
        var engine = new Engine();
        var car = new Car(engine);
        car.Run(3);
        Console.WriteLine(car.Speed);
    }
}

class Engine
{
    public int RPM { get; private set; }

    public void Work(int gas)
    {
        this.RPM = 1000 * gas;
    }
}

class Car
{
    // Car 里面有个 Engine 类型的字段,它两就是紧耦合了
    // Car 依赖于 Engine
    private Engine _engine;

    public int Speed { get; private set; }

    public Car(Engine engine)
    {
        _engine = engine;
    }

    public void Run(int gas)
    {
        _engine.Work(gas);
        this.Speed = _engine.RPM / 100;
    }
}

紧耦合的问题:

  1. 基础类一旦出问题,上层类写得再好也没辙

  2. 程序调试时很难定位问题源头

  3. 基础类修改时,会影响写上层类的其他程序员的工作

所以程序开发中要尽量避免紧耦合,解决方法就是接口。
接口:

  1. 约束调用者只能调用接口中包含的方法

  2. 让调用者放心去调,不必关心方法怎么实现的、谁提供的

接口解耦示例

以老式手机举例,对用户来说他只关心手机可以接(打)电话和收(发)短信。

对于手机厂商,接口约束了他只要造的是手机,就必须可靠实现上面的四个功能。

用户如果丢了个手机,他只要再买个手机,不必关心是那个牌子的,肯定也包含这四个功能,上手就可以用。用术语来说就是"人和手机是解耦的"。

csharp 复制代码
class Program
{
    static void Main(string[] args)
    {
        //var user = new PhoneUser(new NokiaPhone());
        var user = new PhoneUser(new EricssonPhone()); //构造器传参
        user.UsePhone(); //如果我们的EricssPhone手机坏了,我们只需要修改一下传入的参数即可
        //而类里面的方法都不需要修改,用接口可以降低耦合
        Console.ReadKey();
    }
}

class PhoneUser
{
    private IPhone _phone;

    public PhoneUser(IPhone phone)
    {
        _phone = phone;
    }

    public void UsePhone()
    {
        _phone.Dail();
        _phone.PickUp();
        _phone.Receive();
        _phone.Send();
    }
}

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 Tim!");
    }

    public void Send()
    {
        Console.WriteLine("Nokia message ring ...");
    }

    public void Receive()
    {
        Console.WriteLine("Hello!");
    }
}

class EricssonPhone : IPhone
{
    public void Dail()
    {
        Console.WriteLine("Ericsson calling ...");
    }

    public void PickUp()
    {
        Console.WriteLine("Hello! This is Tim!");
    }

    public void Send()
    {
        Console.WriteLine("Ericsson ring ...");
    }

    public void Receive()
    {
        Console.WriteLine("Good evening!");
    }
}

没有用接口时,如果一个类坏了,你需要 Open 它再去修改,修改时可能产生难以预料的副作用。引入接口后,耦合度大幅降低,换手机只需要换个类名,就可以了。等学了反射后,连这里的一行代码都不需要改,只要在配置文件中修改一个名字即可。

在代码中只要有可以替换的地方,就一定有接口的存在;接口就是为了解耦(松耦合)而生。

松耦合最大的好处就是让功能的提供方变得可替换,从而降低紧耦合时"功能的提供方不可替换"带来的高风险和高成本。

  • 高风险:功能提供方一旦出问题,依赖于它的功能都挂

  • 高成本:如果功能提供方的程序员崩了,会导致功能使用方的整个团队工作受阻

依赖反转原则

解耦在代码中的表现就是依赖反转。单元测试就是依赖反转在开发中的直接应用和直接受益者。

人类解决问题的典型思维:自顶向下,逐步求精。

在面向对象里像这样来解决问题时,这些问题就变成了不同的类,且类和类之间紧耦合,它们也形成了这样的金字塔。

依赖反转给了我们一种新思路,用来平衡自顶向下的思维方式。

平衡:不要一味推崇依赖反转,很多时候自顶向下就很好用,就该用。

接口隔离,反射,特性,依赖注入

接口隔离

接口即契约:甲方"我不会多要";乙方"我不会少给"。

●乙方不会少给:硬性规定,即一个类只要实现了接口,就必需实现接口里面的所有方法,一旦未全部实现,类就还只是个抽象类,仍然不能实例化

●甲方不会多要:软性规定,是个设计问题

胖接口及其产生原因

观察传给调用者的接口里面有没有一直没有调用的函数成员,如果有,就说明传进来的接口类型太大了。换句话说,这个"胖"接口是由两个或两个以上的小接口合并起来的。

显式接口实现

接口隔离的第三个例子,专门用来展示 C# 特有的能力 ------ 显式接口实现。

C# 能把隔离出来的接口隐藏起来,直到你显式的使用这种接口类型的变量去引用实现了该接口的实例,该接口内的方法才能被你看见被你使用。

这个例子的背景是《这个杀手不太冷》,WarmKiller 有其两面性,即实现 IGentleman 又实现 IKiller。

之前见过的多接口的默认实现方式,该方式从业务逻辑来说是有问题的,一个杀手不应该总将 Kill 方法暴露出来。

换言之,如果接口里面有的方法,我们不希望它轻易被调用,就不能让它轻易被别人看到。这就要用到接口的显式实现了。

csharp 复制代码
class Program
{
    static void Main(string[] args)
    {
        var wk = new WarmKiller();
        wk.Love(); //类里面的方法都能被调用
        wk.Kill(); 
    }
}
interface IGentleman
{
    void Love();
}
interface IKiller
{
    void Kill();
}
class WarmKiller : IGentleman, IKiller
{
    public void Love()
    {
        Console.WriteLine("I will love you forever ...");
    }
    public void Kill()
    {
        Console.WriteLine("Let me kill the enemy ...");
    }
}

显式接口

csharp 复制代码
static void Main(string[] args)
{
    IKiller killer = new WarmKiller();
    killer.Kill(); //Ikiller类型变量可以调用
    var wk = (WarmKiller) killer;
    wk.Love(); //看不到kill方法
}
class WarmKiller : IGentleman, IKiller
{
    public void Love()
    {
        Console.WriteLine("I will love you forever ...");
    }
    // 显式实现,只有当该类型(WarmKiller)实例被 IKiller 类型变量引用时该方法才能被调用
    void IKiller.Kill()
    {
        Console.WriteLine("Let me kill the enemy ...");
    }
}

反射 Reflection

反射:你给我一个对象,我能在不用 new 操作符也不知道该对象的静态类型的情况下,我能给你创建出一个同类型的对象,还能访问该对象的各个成员。
这相当于进一步的解耦,因为有 new 操作符后面总得跟类型,一跟类型就有了紧耦合的依赖。依靠反射创建类型不再需要 new 操作符也无需静态类型,这样使得很多时候耦合可以松到忽略不计。

反射不是 C# 语言的功能,而是 .NET 框架的功能,所以 .NET 框架支持的语言都能使用反射功能。

C# 和 Java 这种托管类型的语言和 C、C++ 这些原生类型的语言区别之一就是反射。

单元测试、依赖注入、泛型编程都是基于反射机制的。

当程序处于动态期(dynamic)用户已经用上了,不再是开发时的静态期(static)。动态期用户和程序间的操作是难以预测的,如果你要在开始时将所有情况都预料到,那程序的复杂度难免太高,指不定就是成百上千的 if else,即使你真的这样做了,写出来的程序也非常难维护,可读性很低。很多时候更有可能是我们在编写程序时无法详尽枚举出用户可能进行的操作,这时我们的程序就需要一种以不变应万变的能力。

注:

  1. .NET Framework 和 .NET Core 都有反射机制,但它们的类库不太一样,使用反射时用到的一些 API 也略有差别,示例是基于 .NET Core 的

  2. 反射毕竟是动态地去内存中获取对象的描述、对象类型的描述,再用这些描述去创建新的对象,整个过程会影响程序性能,所以不要盲目过多地使用反射

依赖注入

例一:反射的原理以及和反射密切相关的一个重要技能 ------ 依赖注入。

例一沿用的是一开始那个 Tank 的例子。

csharp 复制代码
static void Main(string[] args)
{
    // ITank、HeavyTank 这些都算静态类型
    ITank tank = new HeavyTank();

    // ======华丽的分割线======
    // 分割线以下不再使用静态类型
    var t = tank.GetType();
    object o = Activator.CreateInstance(t);
    MethodInfo fireMi = t.GetMethod("Fire");
    MethodInfo runMi = t.GetMethod("Run");
    fireMi.Invoke(o, null);
    runMi.Invoke(o, null);
}
...

依赖注入框架

依赖注入需要借助依赖注入框架,.NET Core 的依赖注入框架就是:Microsoft.Extensions.DependencyInjection。

依赖注入最重要的元素就是 Container(容器),我们把各种各样的类型和它们对应的接口都放在(注册)容器里面。回头我们创建实例时就找容器要。

容器又叫做 Service Provider

在注册类型时我们可以设置容器创建对象的规则:每次给我们一个新对象或是使用单例模式(每次要的时候容器都给我们同一个实例)。

具体怎么使用容器此处按下不表,主要还是看依赖注入怎么用。

csharp 复制代码
static void Main(string[] args)
{
    // ServiceCollection 就是容器
    var sc = new ServiceCollection();
    // 添加 ITank,并设置其对应的实现类是 HeavyTank
    sc.AddScoped(typeof(ITank), typeof(HeavyTank));
    var sp = sc.BuildServiceProvider();
    // ===========分割线===========
    // 分割线上面是整个程序的一次性注册,下面是具体使用
    ITank tank = sp.GetService<ITank>();
    tank.Fire();
    tank.Run();
}

Driver 创建时本来需要一个 IVehicle 对象,sp 会去容器里面找,又由于我们注册了 IVehicle 的实现类是 Car,所以会自动创建一个 Car 实例传入 Driver 的构造器 。

泛型,partial类,枚举,结构体

泛型(generic)

●为什么需要泛型:避免成员膨胀或类型膨胀

●正交性:泛型类型(类、接口、委托......) 泛型成员(属性、方法、字段......)

●类型方法的参数推断

●泛型与委托,Lambda 表达式

泛型在面向对象中的地位与接口相当。其内容很多,今天只介绍最常用最重要的部分。

基本原理

正交性:泛型和其它的编程实体都有正交点,导致泛型对编程的影响广泛而深刻。

泛化 <-> 具体化

泛型类实例

示例背景:开了个小商店,一开始只卖苹果,卖的苹果用小盒子装上给顾客。顾客买到后可以打开盒子看苹果颜色。

csharp 复制代码
class Program
{
    static void Main(string[] args)
    {
        var apple = new Apple { Color = "Red" };
        var box = new Box { Cargo = apple };
        Console.WriteLine(box.Cargo.Color);
    }
}
class Apple
{
    public string Color { get; set; }
}
class Box
{
    public Apple Cargo { get; set; }
}

后来小商店要增加商品(卖书),有下面几种处理方法:

一:我们专门为 Book 类添加一个 BookBox 类的盒子。

csharp 复制代码
static void Main(string[] args)
{
  	var apple = new Apple { Color = "Red" };
  	var box = new AppleBox { Cargo = apple };
  	Console.WriteLine(box.Cargo.Color);
  
	var book = new Book { Name = "New Book" };
  	var bookBox = new BookBox { Cargo = book };
  	Console.WriteLine(bookBox.Cargo.Name);
}

现在代码就出现了"类型膨胀"的问题。未来随着商品种类的增多,盒子种类也须越来越多,类型膨胀,不好维护。

二:用同一个 Box 类,每增加一个商品时就给 Box 类添加一个属性。

csharp 复制代码
class Program
{
    static void Main(string[] args)
    {
        var apple = new Apple { Color = "Red" };
        var book = new Book { Name = "New Book" };
        var box1 = new Box { Apple = apple };
        var box2 = new Box { Book = book };
    }
}

...

class Book
{
    public string Name { get; set; }
}

class Box
{
    public Apple Apple { get; set; }

    public Book Book { get; set; }
}

这会导致每个 box 变量只有一个属性被使用,也就是"成员膨胀"(类中的很多成员都是用不到的)。

三:Box 类里面的 Cargo 改为 Object 类型。

csharp 复制代码
class Program
{
    static void Main(string[] args)
    {
        var apple = new Apple { Color = "Red" };
        var book = new Book { Name = "New Book" };
        var box1 = new Box { Cargo = apple };
        var box2 = new Box { Cargo = book };
        Console.WriteLine((box1.Cargo as Apple)?.Color);
    }
}

...

class Box
{
    public Object Cargo{ get; set; }
}

使用时必须进行强制类型转换或 as,即向盒子里面装东西省事了,但取东西时很麻烦。

泛型登场

泛型类定义时相当于原材料,使用时再加工成具体的形状

csharp 复制代码
class Program
{
    static void Main(string[] args)
    {
        var apple = new Apple { Color = "Red" };
        var book = new Book { Name = "New Book" };
        var box1 = new Box<Apple> { Cargo = apple }; //使用泛型实体之前要进行泛化,这里泛化为一个装苹果的类型
        //那么Apple类型就替换了Tcargo这个类型
        var box2 = new Box<Book> { Cargo = book };

        Console.WriteLine(box1.Cargo.Color);
        Console.WriteLine(box2.Cargo.Name);
    }
}
class Apple
{
    public string Color { get; set; }
}
class Book
{
    public string Name { get; set; }
}
class Box<TCargo> //泛化需要在类名后加上<>,里面放所要泛化的类型(自定义名)
{
    public TCargo Cargo { get; set; }
}

泛型接口实例

csharp 复制代码
class Program
{
    static void Main(string[] args)
    {
        //var stu = new Student<int>();
        //stu.Id = 101;
        //stu.Name = "Timothy";

        var stu = new Student<ulong>(); //特化为ulong
        stu.Id = 1000000000000001;
        stu.Name = "Timothy";

        var stu2 = new Student();
        stu2.Id = 100000000001;
        stu2.Name = "Elizabeth";
    }
}

interface IUnique<T> //如果有学生类,老师类等都可以继承这个泛型接口
{
    T Id { get; set; }
}

// 泛型类实现泛型接口
class Student<T> : IUnique<T> //因为要继承上面的泛型接口,所以这里也要是泛型
{
    public T Id { get; set; }

    public string Name { get; set; }
}

// 具体类实现特化化后的泛型接口
class Student : IUnique<ulong>
{
    public ulong Id { get; set; }

    public string Name { get; set; }
}

也可以这样写:

csharp 复制代码
class Program
{
    static void Main(string[] args)
    {
        var stu = new Student(); //这里就不用特化了
        stu.Id = 1000000000000001;
        stu.Name = "Timothy";
    }
}
class Student: IUnique<ulong> //也可以在类实现这个泛型接口时,进行特化
{
    public T Id { get; set; }
    public string Name { get; set; }
}

泛型集合

csharp 复制代码
static void Main(string[] args)
{
    IList<int> list = new List<int>(); //动态数组list
    for (var i = 0; i < 100; i++) //Ilist泛型类
    {
        list.Add(i);
    }
    foreach (var item in list)
    {
        Console.WriteLine(item);
    }
}

List定义:

csharp 复制代码
public class List<T> : ICollection<T>, IEnumerable<T>, IEnumerable, IList<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection, IList
{
  ...
}
  • IEnumerable:可迭代
  • ICollection:集合,可以添加和移除元素

泛型委托

action委托

action委托只能引用没有返回值的方法

csharp 复制代码
static void Main(string[] args)
{
    Action<string> a1 = Say;
    a1("Timothy");

    Action<int> a2 = Mul;
    a2(1);
}

static void Say(string str)
{
    Console.WriteLine($"Hello, {str}!");
}

static void Mul(int x)
{
    Console.WriteLine(x * 100);
}
function委托
csharp 复制代码
static void Main(string[] args)
{
    Func<int, int, int> f1 = Add;
    Console.WriteLine(f1(1, 2));

    Func<double, double, double> f2 = Add;
    Console.WriteLine(f2(1.1, 2.2));
}

static int Add(int a, int b)
{
    return a + b;
}

static double Add(double a, double b)
{
    return a + b;
}

配合 Lambda 表达式:

csharp 复制代码
//Func<int, int, int> f1 = (int a, int b) => { return a + b; };
Func<int, int, int> f1 = (a, b) => { return a + b; };
Console.WriteLine(f1(1, 2));

partial 类

减少派生类

学习类的继承时就提到过一个概念,"把不变的内容写在基类里,在子类里写经常改变的内容"。这就导致一个类中只要有经常改变的内容,我们就要为它声明一个派生类,如果改变的部分比较多,还得声明多个或多层派生类,导致派生结构非常复杂。

有 partial 类后,我们按照逻辑将类切分成几块,每块作为一个逻辑单元单独更新迭代,这些分块合并起来还是一个类。

枚举类型

  • 人为限定取值范围的整数
  • 整数值的对应
  • 比特位式用法

枚举示例

如何设计员工类的级别属性。

  1. 使用数字? 大小不明确
  2. 使用字符串? 无法约束程序员的输入

使用枚举,即限定输入,又清晰明了:

csharp 复制代码
class Program
{
    static void Main(string[] args)
    {
        var employee = new Person
        {
            Level = Level.Employee
        };
        var boss = new Person
        {
            Level = Level.Boss
        };
        Console.WriteLine(boss.Level > employee.Level);
        // True
        Console.WriteLine((int)Level.Employee);// 0
        Console.WriteLine((int)Level.Manager); // 100
        Console.WriteLine((int)Level.Boss);    // 200
        Console.WriteLine((int)Level.BigBoss); // 201
    }
}

enum Level
{
    Employee,
    Manager = 100,
    Boss = 200,
    BigBoss,
}

class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Level Level { get; set; }
}

枚举的比特位用法

csharp 复制代码
class Program
{
    static void Main(string[] args)
    {
        var employee = new Person
        {
            Name = "Timothy",
            Skill = Skill.Drive | Skill.Cook | Skill.Program | Skill.Teach
            //将enum按位或可以让这个对象拥有多个技能
        };
        Console.WriteLine(employee.Skill); // 15
        
        // 过时用法不推荐
        //Console.WriteLine((employee.Skill & Skill.Cook) == Skill.Cook); // True
        
        // .NET Framework 4.0 后推荐的用法
        Console.WriteLine((employee.Skill.HasFlag(Skill.Cook))); // True
    }
}

[Flags]
enum Skill
{
    Drive = 1, //二进制0001
    Cook = 2, //0010
    Program = 4, //0100
    Teach = 8, //1000
}

class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Skill Skill { get; set; }
}

结构体

  • 值类型,可装/拆箱
  • 可实现接口,不能派生自类/结构体
  • 不能有显式无参构造器
csharp 复制代码
class Program
{
    static void Main(string[] args)
    {
        Student stu = new Student { Id = 101, Name = "Timothy" };
        // 装箱:复制一份栈上的 stu ,放到堆上去,然后用 obj 引用堆上的 student 实例
        object obj = stu;

        // 拆箱
        Student stu2 = (Student)obj; //强制
        Console.WriteLine($"#{stu2.Id} Name:{stu2.Name}");
    }
}

struct Student
{
    public int Id { get; set; }

    public string Name { get; set; }
}

因为是值类型,所以拷贝时是值复制:

csharp 复制代码
static void Main(string[] args)
{
    var stu1 = new Student { Id = 101, Name = "Timothy" };

    // 结构体赋值是值复制
    var stu2 = stu1;
    stu2.Id = 1001;
    stu2.Name = "Michael";

    Console.WriteLine($"#{stu1.Id} Name:{stu1.Name}"); 
    //如果Student为类,输出1001 Michael,因为类为引用类型
    //输出为 #101 Name:Timothy
}

结构体可以实现接口

csharp 复制代码
class Program
{
    static void Main(string[] args)
    {
        var stu1 = new Student { Id = 101, Name = "Timothy" };
        stu1.Speak();
    }
}

interface ISpeak //接口
{
    void Speak();
}

struct Student : ISpeak //
{
    public int Id { get; set; }
    public string Name { get; set; }
    public void Speak() //实现抽象方法
    {
        Console.WriteLine($"I'm #{Id} student {Name}");
    }
}

注:

  1. 将结构体转换为接口时要装箱
  2. 结构体不能有基类或基结构体,只可以实现接口
  3. 结构体不能有显示无参构造器

总结

因为之前模模糊糊的大致过了一遍,可以看我上一次发布的关于c#的文章,真的是毫无逻辑。这次花了4天学完了,能够听一个好老师讲学着会比较清晰,
刘铁猛《C#语言入门详解》全集,在b站上一直听这个老师的课,讲的很好,真的是深入浅出。因为我也是初学者某些地方也不太懂,只能把我学习过程的笔记整理一下供大家参考。

相关推荐
tanyongxi6623 分钟前
C++ AVL树实现详解:平衡二叉搜索树的原理与代码实现
开发语言·c++
阿葱(聪)1 小时前
java 在k8s中的部署流程
java·开发语言·docker·kubernetes
浮生带你学Java2 小时前
2025Java面试题及答案整理( 2025年 7 月最新版,持续更新)
java·开发语言·数据库·面试·职场和发展
斯是 陋室2 小时前
在CentOS7.9服务器上安装.NET 8.0 SDK
运维·服务器·开发语言·c++·c#·云计算·.net
李长渊哦3 小时前
深入理解Java中的Map.Entry接口
java·开发语言
koooo~3 小时前
JavaScript中的Window对象
开发语言·javascript·ecmascript
tju新生代魔迷3 小时前
C++:list
开发语言·c++
夜月蓝汐3 小时前
JAVA中的Collection集合及ArrayList,LinkedLIst,HashSet,TreeSet和其它实现类的常用方法
java·开发语言
肥or胖3 小时前
【FFmpeg 快速入门】本地播放器 项目
开发语言·qt·ffmpeg·音视频