c#笔记-定义类

声明类

类可以使用帮助你管理一组相互依赖的数据,来完成某些职责。
类使用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;
		}
	}
}

命名约定

具有publicprotected修饰的所有类成员,都意味着将会被他人使用。
为了代码风格的一致性,约定公开的类成员,以驼峰命名法。

  1. 使用英文作为名字
  2. 把所有的空格后的第一个字母大写,然后去掉所有空格
  3. 把首字母大写。

例如:超光速

  1. faster than light
  2. fasterThanLight
  3. FasterThanLight

不公开的命名规范,不好说。我随手翻了一些下的扩展包,就见到了一堆不同命名风格。

封装

为什么要阻止调用者访问所有的东西呢?

  • 对于不想知道所有东西的人来说:如果把你带到飞机上的驾驶舱,你看着眼前琳琅满目的按钮你会怎么想?但是,如果我只给你6个键,上下左右前后。你是不是敢说你也会开飞机了。
  • 对于想知道所有东西的人来说:不允许外部修改一些关键数据。例如我游戏只做了70关。然后调用者一改,跳关到80关。那我怎么运行呢?
  • 对于你自己写的代码而言,你可能觉得没有必要,因为想改就改。但是请记住,真实的开发绝对不会是靠你一个人就能完成的。你可能会下扩展包,可能会用你的同学同事写好的代码。有可能很久以后你想起来你写的一个代码刚好能解决问题,但是已经看不懂你写的具体内容。

职责

在定义类的时候,应该先想好它应该做什么,规划他的职责范围。
在你只有一个地方使用的时候,你可能觉得无所谓,写谁身上都一样。

人物
食物
调用
修改
生命值
恢复生命
牛肉
食物
人物
修改
调用
牛肉
生命值
恢复生命

但是如果东西多起来了,可能就会出现重复的代码。

人物
治疗师
食物
调用
调用
调用
修改
修改
修改
生命值
恢复生命3
村民
恢复生命2
旅馆
恢复生命1
牛肉

但是如果把恢复生命的方法,写在人物里面,那么就只需要写一份恢复生命
而且本来这个操作修改的也是人物自己的东西,恢复生命,本应是人物的职责。
这样,以后不管是谁使用人物,都可以直接使用他的恢复生命,不需要自己写一份了。

治疗师
食物
人物
修改
调用
调用
调用
村民
旅馆
牛肉
生命值
恢复生命

类成员

方法中的变量有作用域,可以根据代码块判断它们何时不再需要,立刻释放内存。
但是类中的变量无法这样判断,因为它们可能被类中的任何一个方法所使用,而方法的调用时机是不确定的。
所以类中的变量不会被单独清除,它们只会随着类的实例化和销毁而分配和回收内存。
只有当整个类的对象都不再被引用时,它们才会被一起清除。
由于类的生命周期是动态的,所以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# 中,这种区分并不是很重要。即便是编写的局部函数,在编译后也会变成类成员。

重载

作为类成员的方法,具有重载的性质。
不同的方法之间可以同名,只要他们的参数列表不同(数量,类型,顺序)。
引用参数和基本类型的参数不一样,可以重载。
但他们之间都是相同的引用参数,不能只有inoutref不同的情况下重载。

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

在调用方法时,编译器会查找更为具体的方法。

  1. 如果调用方法的参数列表有一个直接匹配的重载,那么会忽略掉不定参数的重载。
  2. 检查所有参数的隐式转换和自身,如果方法重载有参数更为具体(int)的参数,则忽略掉更抽象的(object)重载。
  3. in参数不需要在调用时添加in,但如果用in引用参数和普通参数重载,则根据调用时是否有in决定重载。
  4. 如果没有或有多个这样的匹配方法,则会报错。

例如这种情况下,调用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访问器是一个返回值和属性类型一样的无参方法。
setinit访问器是仅有一个类型和属性一样,名为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是一个操作符,

  1. 首先会计算类型占用的空间
  2. 找一个合适的地方分配空间
  3. 取地址
  4. 执行构造器

虽然取地址在执行构造器之前,但赋值语句总是最慢的,
它会等待构造器完成执行才会执行赋值。

如果你没有写任何构造器,编译器会帮你弄一个没有参数的公开构造器。
在你写了以后就不会生成这个了。但是记住,构造器默认权限也是私有的

构造器链

构造器比较特殊,如果要递归调用,只能出现在另一个构造器的开头。
并且使用特殊语法。使用:表示首先执行,使用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运算符,他将能作为条件,放入到ifwhile.三元运算符中作为条件。
不过,他还是不能直接给bool变量赋值或是以其他形式当作bool

虽然true运算符要求同时重载false运算符,但false的作用极其有限。
作为条件时只会使用true运算符。false运算符唯一的作用是

  1. 你需要重载&运算符
  2. 你的&运算符的返回值类型要和自己一样
  3. 然后你就能使用&&逻辑运算符,运算规则是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;

在你的控制台模板项目生成时,就带有一些默认的全局引用。
可以在你的编译器左上角看到他们。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/zms9110750/article/details/130543225