声明类
类可以使用帮助你管理一组相互依赖的数据,来完成某些职责。
 类使用class关键字定义,并且必须在所有顶级语句之下。
 类的成员只能有声明语句,不能有执行语句。
class Player1
{
	int Hp;
	int MaxHp;
	int Atk;
	int Def;
	int Overflow()
	{
		if (Hp < 0)
		{
			int i = Hp;
			Hp = 0;
			return i;
		}
		else if (Hp > MaxHp)
		{
			int i = Hp - MaxHp;
			Hp = MaxHp;
			return i;
		}
		else
		{
			return 0;
		}
	}
}
 
使用new+类名+括号可以构造出一个这种类型的值。但无法访问里面的东西。
Player1 player1 = new Player1();
player1.Hp = 400;//不可访问,因为它具有一定的保护级别
 
访问权限
如果希望外部可以访问到你定义类的成员,你需要公开他们的访问权限。
 可以在所有的声明前面加上public,这样你将可以在外部任意访问和修改他们。
Player2 player2 = new Player2();
player2.Hp = 400;
class Player2
{
	public int Hp;
	public int MaxHp;
	public int Atk;
	public int Def;
	public int Overflow()
	{
		if (Hp < 0)
		{
			int i = Hp;
			Hp = 0;
			return i;
		}
		else if (Hp > MaxHp)
		{
			int i = Hp - MaxHp;
			Hp = MaxHp;
			return i;
		}
		else
		{
			return 0;
		}
	}
}
 
访问权限列表
| 调用方的位置 | public(公开) | protected internal | protected(保护) | internal(内部) | private protected | private(私有) | 
|---|---|---|---|---|---|---|
| 内部 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | 
| 派生类 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | 
| 相同程序集 | ✔️ | ✔️ | ❌ | ✔️ | ❌ | ❌ | 
| 不同程序集的派生类 | ✔️ | ✔️ | ✔️ | ❌ | ❌ | ❌ | 
| 任何 | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | 
默认访问权限
| 成员 | 默认(没写修饰符时)可访问性 | 允许的成员的声明的可访问性 | 
|---|---|---|
| 命名空间 | public | 无(不能添加修饰符) | 
| 枚举 | public | 无(不能添加修饰符) | 
| 顶级类 | internal | internal 或 public | 
| 嵌套类 | private | 全部 | 
| 类成员 | private | 全部 | 
| 嵌套结构 | private | 除了带有protected的访问权限。因为派生对他不可用。 | 
内部类
类可以声明在其他的类中。这样的类也可以使用他的私有成员。
internal class Barrack
{
	private int level;
	private class Marine
	{
		public int Atk;
		public void Init(Barrack barrack)
		{
			Atk += barrack.level;
		}
	}
}
 
命名约定
具有public或protected修饰的所有类成员,都意味着将会被他人使用。
 为了代码风格的一致性,约定公开的类成员,以驼峰命名法。
- 使用英文作为名字
 - 把所有的空格后的第一个字母大写,然后去掉所有空格
 - 把首字母大写。
 
例如:超光速
- faster than light
 - fasterThanLight
 - FasterThanLight
 
不公开的命名规范,不好说。我随手翻了一些下的扩展包,就见到了一堆不同命名风格。
封装
为什么要阻止调用者访问所有的东西呢?
- 对于不想知道所有东西的人来说:如果把你带到飞机上的驾驶舱,你看着眼前琳琅满目的按钮你会怎么想?但是,如果我只给你6个键,上下左右前后。你是不是敢说你也会开飞机了。
 - 对于想知道所有东西的人来说:不允许外部修改一些关键数据。例如我游戏只做了70关。然后调用者一改,跳关到80关。那我怎么运行呢?
 - 对于你自己写的代码而言,你可能觉得没有必要,因为想改就改。但是请记住,真实的开发绝对不会是靠你一个人就能完成的。你可能会下扩展包,可能会用你的同学同事写好的代码。有可能很久以后你想起来你写的一个代码刚好能解决问题,但是已经看不懂你写的具体内容。
 
职责
在定义类的时候,应该先想好它应该做什么,规划他的职责范围。
 在你只有一个地方使用的时候,你可能觉得无所谓,写谁身上都一样。
但是如果东西多起来了,可能就会出现重复的代码。
但是如果把恢复生命的方法,写在人物里面,那么就只需要写一份恢复生命。
 而且本来这个操作修改的也是人物自己的东西,恢复生命,本应是人物的职责。
 这样,以后不管是谁使用人物,都可以直接使用他的恢复生命,不需要自己写一份了。
类成员
方法中的变量有作用域,可以根据代码块判断它们何时不再需要,立刻释放内存。
 但是类中的变量无法这样判断,因为它们可能被类中的任何一个方法所使用,而方法的调用时机是不确定的。
 所以类中的变量不会被单独清除,它们只会随着类的实例化和销毁而分配和回收内存。
 只有当整个类的对象都不再被引用时,它们才会被一起清除。
 由于类的生命周期是动态的,所以class声明的类都是引用类型。
字段
在方法内部定义的变量和方法,称为局部变量和局部方法。
局部一词也可以用本地替换,因为它们都是英文单词local的翻译。
类中直接定义的变量,称为字段,它们是类的成员之一。
 字段必须指定数据类型,不能使用var进行隐式类型推断。
 字段可以在声明时赋予初始值,但是不能使用其他的实例成员参与赋值表达式。
class Person
{
    string name = "Alice"; // 可以赋予常量值
    int age = GetAge(); // 错误,不能使用实例方法参与赋值
    double height = weight * 0.9; // 错误,不能使用其他实例字段参与赋值
    double weight; // 可以不赋值
    int GetAge()
    {
        return 12;
    }
}
 
只读
一个字段可以使用readonly修饰。让他只能保持初始值。
只读只能保证这个变量不被更改。一些引用类型可以在不改变变量的情况下改变值。
class Sudent
{
	public readonly int Age = 60;
	public readonly string Name = "小明";
}
 
方法
在一些语言中,方法和函数有明确的区别,类的成员才能叫方法。以这个观点局部方法只能称为局部函数。
但在 C# 中,这种区分并不是很重要。即便是编写的局部函数,在编译后也会变成类成员。
重载
作为类成员的方法,具有重载的性质。
 不同的方法之间可以同名,只要他们的参数列表不同(数量,类型,顺序)。
 引用参数和基本类型的参数不一样,可以重载。
 但他们之间都是相同的引用参数,不能只有in,out,ref不同的情况下重载。
class Printer
{
    public void Print(int number)
    {
        Console.WriteLine($"这个数字是 {number}。");
    }
    public void Print(string message)
    {
        Console.WriteLine($"这个信息是 {message}。");
    }
    
    public void Print(ref string message)
    {
        Console.WriteLine($"这个信息是 {message}。");
    }
    public void Print(double value, string unit)
    {
        Console.WriteLine($"这个数值是 {value} {unit}。");
    }
}
 
在调用方法时,编译器会查找更为具体的方法。
- 如果调用方法的参数列表有一个直接匹配的重载,那么会忽略掉不定参数的重载。
 - 检查所有参数的隐式转换和自身,如果方法重载有参数更为具体(int)的参数,则忽略掉更抽象的(object)重载。
 in参数不需要在调用时添加in,但如果用in引用参数和普通参数重载,则根据调用时是否有in决定重载。- 如果没有或有多个这样的匹配方法,则会报错。
 
例如这种情况下,调用new Calculator().Add(40,20);就会出现歧义。
class Calculator
{
    public void Add(int x, object y)
    {
        switch (y)
        {
            case string s:
                Console.WriteLine(x + s);
                break;
            case int i:
                Console.WriteLine(x + i);
                break;
            default:
                Console.WriteLine("Invalid argument");
                break;
        }
    }
    public void Add(object x, int y)
    {
        switch (x)
        {
            case string s:
                Console.WriteLine(s + y);
                break;
            case int i:
                Console.WriteLine(i + y);
                break;
            default:
                Console.WriteLine("Invalid argument");
                break;
        }
    }
}
 
属性
在早期,如果使用者希望获得,或修改类成员的变量,必须使用方法
class Hp1
{
	private int now;
	private int max;
	public int GetNow()
	{
		return now;
	}
	public void SetNow(int value)
	{
		now = Math.Clamp(value, 0, max);
	}
}
 
因为封装的特性,所有对值的修改应该是在自己的可控范围,
 或者是自己应该收到通知,执行一些操作。
属性可以简化这个过程,它能声明一个像字段的方法。
class Hp2
{
	private int now;
	private int max;
	public int Now { get => now; set => now = Math.Clamp(value, 0, max); }
}
 
get访问器
属性的使用方式和字段类似,但必须具有get访问器的属性才能够获取值。
 对于返回引用变量的属性,只允许存在get访问器
Hp3 Hp3 = new Hp3();
int hp3 = Hp3.Now;//错误,这个属性无法取值
class Hp3
{
	private int now;
	private int max;
	public int Now { set => now = Math.Clamp(value, 0, max); }
	public int Now2 { get => now; }
	public ref int Max { get => ref max; }
}
 
set访问器
具有set访问器的属性,才能像变量一样进行赋值。
Hp4 Hp4 = new Hp4();
Hp4.Now = 40;//错误,这个属性无法赋值
class Hp4
{
	private int now;
	private int max;
	public int Now { get => now; }
	public int Now2 { set => now = Math.Clamp(value, 0, max); }
}
 
init访问器
属性无法使用readonly进行修饰。作为补偿,可以使用init访问器来代替set访问器。
 init访问器仅能在赋值初始值时使用。并且这个访问器可以操作只读的字段。
class Hp5
{
	private int now;
	private readonly int max;
	public int Now { get => now; }
	public int Max { get => max; init => max = value; }
}
 
自动实现属性
如果一个属性对字段的控制不需要逻辑,并且有get访问器,
 可以省略掉访问器的逻辑。编译器会自动生成一个字段交给他控制。
 并且,你可以对这个属性进行赋值,来给这个自动生成的字段赋初始值。
class Student
{
    public string Name { get; set; } = "张三";
    public int Age { get; init; }
    public string Password { get; } = "password";//至少要有get属性
}
 
访问器的访问权限
访问器的get,set,init都可以单独设置一个访问权限。
 但至少要存在一个没有设置权限的访问器,并且访问器的权限必须低于属性的权限。
class Counter
{
	public int Count { get; private set; }
	public void Increment()
	{
		Count++;
	}
}
 
lamda表达式
=>只能对单行语句使用。如果逻辑复杂,可以扩展为完整的方法。
 get访问器是一个返回值和属性类型一样的无参方法。
 set和init访问器是仅有一个类型和属性一样,名为value参数的无返回值方法,
class AccessCounter
{
	private int accessCount;
	public int AccessCount
	{
		get
		{
			int currentCount = accessCount;
			accessCount++;
			return currentCount;
		}
		set
		{
			accessCount = value;
		}
	}
}
 
反过来,如果内容只有一条语句,那么方法也可以使用lamda表达式。
class AccessCounter2
{
	private int accessCount;
	public int GetAccessCount() => accessCount++;
	public void SetAccessCount(int value) => accessCount = value;
}
 
何时使用属性
属性的介绍是像使用字段一样使用方法。
 所以它有了字段的特点:无法作为语句单独放置。
 而方法可以作为语句单独放置,因为方法会执行操作,会改变一些东西。
所以,如果不会改变太多的东西,即只改变自己控制的那些字段,就使用属性。
 甚至有的时候,属性只给读取值,什么也不改变。
 对于仅get属性,还能够再简写。去掉属性的大括号和get,直接用=>返回。
class Hero
{
	public int Hp { get; private set; }
	public int MaxHp { get; private set; }
	public double HpRatio => 1.0 * Hp / MaxHp;
}
 
索引器
索引器和属性类似,使用访问器来简化方法调用。他牺牲了名字换取了参数。
class StringCollection
{
	private string[] arr = new string[100];
	public string this[int i]
	{
		get => arr[i];
		set => arr[i] = value;
	}
}
 
索引器的名字必须是this。他的参数使用[]来代替(),且不能没有参数。
 索引器的调用方式类似于数组的索引。
StringCollection sc = new StringCollection();
sc[0] = "Hello";
sc[1] = "World";
Console.WriteLine(sc[0]); //Hello
Console.WriteLine(sc[1]); //World
 
构造器
构造器是定义初始字段的地方。构造器没有返回值,方法名和类名一样。他有一些特点:
- 必定会被调用,且先于其他方法被调用。
 - 只会执行一次。
 - 可以为只读字段赋值。
 
class Circle
{
	private readonly double radius;
	public double Area => radius * radius * Math.PI;
	public double Perimeter => 2 * radius * Math.PI;
	public Circle(double radius)
	{
		this.radius = radius;
	}
}
 
因为这些特点,构造器很适合用来初始化字段的初始值。
new
构造器的调用方法是new+构造器
Circle circle = new Circle(20);
 
构造器只能搭配new进行调用。new是一个操作符,
- 首先会计算类型占用的空间
 - 找一个合适的地方分配空间
 - 取地址
 - 执行构造器
 
虽然取地址在执行构造器之前,但赋值语句总是最慢的,
 它会等待构造器完成执行才会执行赋值。
如果你没有写任何构造器,编译器会帮你弄一个没有参数的公开构造器。
在你写了以后就不会生成这个了。但是记住,构造器默认权限也是私有的。
构造器链
构造器比较特殊,如果要递归调用,只能出现在另一个构造器的开头。
 并且使用特殊语法。使用:表示首先执行,使用this表示构造器。
Contract
{
	private int id;
	public string PartyA { get; private set; }
	public string PartyB { get; private set; }
	public Contract(int id)
	{
		this.id = id;
		Console.WriteLine($"员工{id}进入打印室");
	}
	public Contract(string PartyA, int id) : this(id)
	{
		this.PartyA = PartyA;
	}
	public Contract(string PartyA, string PartyB, int id) : this(PartyA, id)
	{
		if (id.ToString().Length == 4)
			this.PartyB = PartyB;
		else
			Console.WriteLine("检测到违规操作");
	}
}
 
终结器
和构造器相反,终结器是在一个对象被清理时触发的。
 它由.Net调用,我们无法主动调用他,所以不能有参数,也不能有访问修饰符。
 他的语法是在构造器前加一个~
class Waste
{
	~Waste()
	{
		Console.WriteLine("一个垃圾被清理了");
	}
}
 
不过.Net不是时刻监视引用类型是否不再使用的,只会在觉得内存占用过多,
 或内存不够的时候执行一次清理。所以如果想观察到它,需要创建很多对象。
for (int i = 0; i < 1000_000; i++)
{
	new Waste();
}
 
解构方法
解构方法可以让实例能像元组一样被析构,或使用模式匹配的位置模式。
 解构方法是公开无返回值的,名为Deconstruct的方法。所有参数均为out参数。
class Point
{
	public int X;
	public int Y;
	public void Deconstruct(out int x, out int y)
	{
		x = X;
		y = Y;
	}
	public void Deconstruct(out int x, out int y, out double length)
	{
		(x, y) = this;
		length = Math.Sqrt(x * x + y + y);
	}
}
 
实例和静态
类和实例这两个术语不是好名字。不能直接从名字看出来他是什么。
 让我们换成民法中的术语种类物和特定物
种类物,比如说钱。我给你的一百元,五十元,和一堆硬币,都是钱。
 你不在乎是哪个钱,只要是钱就行。
特定物,比如说照片。有人把你的全家福弄坏了,把他的旅游照赔给你,你会同意吗?
 那肯定不同意,但是为什么?明明都是照片,为什么不一样呢?
 特定物的含义就是只有这个东西才行。不能用其他的同种东西替代。
实例,就是能说这个的东西。比如这只猫,那棵树,我家的狗。这些都能说这个。
 类,是一个抽象概念。例如鸭子有两条腿,这里的鸭子不是说哪只真正存在的鸭子,只指代鸭子这个概念。
重量,年龄这种属性,是实例属性。必须指出是哪一个才能讨论。
 比如只能说我家的猫3岁,不能说猫是3岁。
静态成员
在一个成员前加static修饰就会变成静态的。
 实例成员对于每一个实例都是不一样的,他们互不干扰。
 但是静态成员是跟随类的,所有实例访问到的都是同一份静态成员。
Cat cat1 = new Cat(30, 10);
Cat cat2 = new Cat(20, 20);
cat1.Show();
cat2.Show();
cat1.Weight = 60;
cat1.Height = 50;
Cat.Legs = 8;//静态成员必须直接通过类名访问
cat1.Show();
cat2.Show();//实例字段没有变化,但腿的数量变成了8
 
class Cat
{
	public int Height;
	public int Weight;
	public static int Legs;
	public Cat(int height, int weight)
	{
		Height = height;
		Weight = weight;
	}
	public void Show()
	{
		Console.WriteLine("身高" + Height);
		Console.WriteLine("体重" + Weight);
		Console.WriteLine("腿的数量" + Legs);
		Console.WriteLine("===============");
	}
}
 
this
静态成员也可以有属性,方法,字段。但唯独不能有索引器。因为this的含义就是当前实例。
 索引器的语法this[ index]的this就是表示你的变量,只是声明的时候不知道你变量叫什么。
在方法的内部可以通过this访问实例成员,通过类名访问静态成员。这在参数和成员同名时很有用。
class Dog
{
	public int Height;
	public int Weight;
	public static int Legs;
	public Dog(int Height, int Weight, int Legs)
	{
		this.Height = Height;
		this.Weight = Weight;
		Dog.Legs = Legs;
	}
}
 
静态构造器
静态字段同样可以有只读字段,也同样只能在构造器里修改。
 不同的时,静态字段的初始值可以使用其他静态成员参与表达式中。
 会按照顺序赋值,还没有赋值的字段在表达式中会以默认值计算。
静态构造器会在这个类第一次被访问时(不是程序启动时)由.Net调用,
 所以同样不能添加访问修饰符和参数。
静态成员先于所有实例创建。实例字段的初始值可以使用静态成员参与表达式。
class Duck
{
	public int Height;
	public int Weight;
	public static readonly int Legs;
	static Duck()
	{
		Legs = 2;
		Console.WriteLine("鸭子有两条腿");
	}
}
 
Console.WriteLine("还没有使用鸭子");
Console.WriteLine(Duck.Legs);
 
静态类
可以给类声明为静态,这样就无法创建他的实例。
 无法创建实例的静态类,讲无法拥有任何实例成员,包括编译器自动添加的无参构造器。
 一般来说都是一些只有方法的工具类才这样做。
static class HelloWorld
{
	public static void Hello(string name)
	{
		Console.WriteLine("你好," + name);
	}
}
 

扩展方法
在顶级(不是内部类)静态类中,方法的第一个参数可以添加this进行修饰。
 在使用this修饰的类型的值时,可以像调用实例方法一样调用这个静态方法。
static class Tool
{
	public static void Hello(ref this bool b)
	{
		b = !b;
		Console.WriteLine(b + "被逆转了");
	}
	public static void Hello(this string s)
	{
		Console.WriteLine("你好" + s);
	}
}
 
string s1 = "世界";
bool b1 = true;
s1.Hello();//像实例方法调用
b1.Reverse();//不需要添加ref,修改会作用到这个变量上
Tool.Hello(s1);//也能正常用静态方法调用。
Tool.Reverse(ref b1);//只有值类型才能声明ref扩展方法
 
运算符重载
使用operator可以为这个类型定义运算符,一些规则如下
- 参数至少有一个是自己类型
 - 对于二元运算,参数的顺序是有影响的(有些运算不满足交换律)
 - 不能在双方类型定义相同顺序和类型的运算符,会出现歧义
 - 必须有返回值,不能是void
 - 一些运算符必须成对出现,但对于返回bool的,不要求互斥。
 
关于哪些运算符可以重载,请参阅之前的文章
class Speed
{
	public int MetrePerSecond;
	public Speed(int metrePerSecond = 0)
	{
		MetrePerSecond = metrePerSecond;
	}
	public static bool operator !(Speed L) => L.MetrePerSecond != 0;
	public static int operator ~(Speed L) => -L.MetrePerSecond;
	public static Speed operator +(Speed L, Speed R) 
		=> new Speed(L.MetrePerSecond + R.MetrePerSecond);
}
 
自增和自减
++和--要求返回值类型必须是自己。
 当一个变量完成了++或--后,这个变量会执行一个赋值操作,
 用这个运算符的返回值将他替换。
true和false
一个类型可以重载true运算符,他将能作为条件,放入到if,while.三元运算符中作为条件。
 不过,他还是不能直接给bool变量赋值或是以其他形式当作bool。
虽然true运算符要求同时重载false运算符,但false的作用极其有限。
 作为条件时只会使用true运算符。false运算符唯一的作用是
- 你需要重载
&运算符 - 你的
&运算符的返回值类型要和自己一样 - 然后你就能使用
&&逻辑运算符,运算规则是false(x) ? x : x & y 
自定义类型转换
类型转换使用implicit(隐式),explicit(显示)之一,加上operator指定。
 参数和返回值其中有一个是自身的类型。
class Electric
{
	public static explicit operator Magnetism(Electric L) => new Magnetism();
}
class Magnetism
{
	public static implicit operator Electric(Magnetism L) => new Electric();
}
 
转换没有传递性,但每一个隐式转换都可以以显示转换调用。
 有必要的话可能需要使用这种方式转换(生物能)(化学能)(热能)电能。
命名空间
定义命名空间
类同样不能重名。为了区分类,可以使用命名空间隔离他们。
 命名空间的作用类似于文件夹。不同文件夹下的文件是可以同名的。
namespace Plain//郊外
{
	namespace Castle//古堡
	{
		class Ghost
		{ }//幽灵
	}
	class wildBoar
	{ }//野猪
}
 
声明命名空间时可以一次性声明多层级的命名空间,使用.隔开
namespace Plain.Castle
{
	class Candle//诡异的蜡烛
	{ }
}
 
使用文件命名空间,可以指定该文件下所有类都处于此空间中。
 但不能再声明其他命名空间,或使用顶级语句。
namespace Plain.Castle;
 
完全限定名
在调用有重名的类或没有引用命名空间时,
 需要带上他的完整命名空间名。
 对于没有命名空间的,使用global::(对,是两个冒号)表示根路径。
class Boo { }
namespace A.B.C
{
	class Boo { }
}
 
调用:
global::Boo boo1 = new global::Boo();
A.B.C.Boo boo2 = new A.B.C.Boo();
 
引用命名空间
在文件的开头,或一个命名空间的类定义之前,可以使用using引用命名空间。
 引用命名空间后,在作用域内使用这些命名空间下的类不需要再写完全限定名。
namespace A.B.C
{
	class Foo { }
}
 
using A.B.C;
Foo foo = new Foo();
 
类型别名
使用using可以用类似变量赋值的操作,给一个类型指定一个别名。
namespace Gas
{
	class CarbonDioxide { }
}
 
using CO2 = Gas.CarbonDioxide;
CO2 co2 = new CO2();
 
静态引用
使用static using可以导入一个类型的所有静态成员,
 在不点出他的类名的情况下使用他的静态成员。
using static System.Int32;//int关键字就是这个类型的类型别名
int int32 = Parse("32");
int max = MaxValue;
 
类的成员常量也会被认为是静态成员。
全局引用
使用global修饰的命名空间引用,类型别名,静态引用,会作用到这个程序集下的所有文件。
global using System;
 
在你的控制台模板项目生成时,就带有一些默认的全局引用。
 可以在你的编译器左上角看到他们。
 



















