声明类
类使用class
关键字定义,并且必须在所有顶级语句之下。
类的成员只能有声明语句,他们可以使用执行语句赋值初始值。
但不能在脱离声明语句的情况下单独使用执行语句。
class C1
{
string s = Console.ReadLine();//声明一个变量
Console.ReadLine();//不跟随声明语句单独使用执行语句
}
字段
直属于类下的东西称为类成员。字段是指成员变量
方法中的变量称为局部变量,在方法执行结束的时候就会清除。
而类成员只有等到整个类实例都没人用了才能一起清除。
没有办法单独判断哪一个成员是以后不会再用的。
字段可以在声明时赋值。但不能使用其他实例成员参与表达式。
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
决定重载。 - 如果没有或有多个这样的匹配方法,则会报错。
例如Add( 40 , 40)
对于以下两个方法会发生歧义。
class Calculator
{
public void Add(int x, object y)
{
//内容略
}
public void Add(object x, int y)
{
//内容略
}
}
属性
属性是方法和字段的结合体。
结合方式,在编译后的执行是以方法的形式执行。
在编写源码时以变量的语法来书写。
说白了就是简化方法的调用,把调用方法变得跟直接访问变量似的。
class Damage
{
int value;
int Value
{
get
{
return Math.Max(0, value);
}
set
{
value = Math.Max(0, value);
//经典问题,伤害如果是负数会不会给对方回血。
//属性可以用来校验得到的值进行过滤,来保证获得的值是合法的。
}
}
void Invok()
{
Console.WriteLine(Value);
Value += 10;//访问属性就像访问变量一样
}
}
get访问器
一个属性至少需要有一个访问器。
在把属性当作变量用的时候,对其进行取值会执行他的get
访问器。
如果没有get
访问器,那么这个属性就不允许执行取值操作。
get
访问器是一个无参的,返回值为属性类型的方法。
set访问器
在把属性当作变量用的时候,对其进行赋值会执行他的set
访问器。
没有set
访问器的属性是不允许进行赋值操作的。
set
访问器是一个有参数,无返回值的方法。
这个参数通过关键字value
访问,值为当作变量赋值时用于赋值的值。
init访问器
init
访问器用于替代set
访问器。类似于只读的字段。init
访问器只允许在构造期间使用。
class Hp
{
int now;
readonly int max;
int Now
{
get
{
return now;
}
}
int Max
{
get
{
return max;
}
init
{
max = value;
}
}
}
lamda表达式
如果方法的主体只有一条语句。那么可以用=>
连接这条语句,并省略大括号和return
。
这对访问器同样有效。并且如果只有get
访问器,还能省略get
和属性的大括号。
但是,通常只给访问器使用。因为访问器外部有一对属性的大括号。即便省略大括号也仍然显眼。
class Mp
{
int now;
readonly int max;
int Now => now;
int Max
{
get => max;
init => max = value;
}
}
自动属性
如果一个属性至少具有get
访问器。那么他的所有访问器都可以不设置逻辑。
class Student
{
string Name { get; set; } = "张三";
int Age { get; init; }
string Password { get; } = "password";//至少要有get属性
}
这种情况下,可以对自动属性进行赋值。
而编译器会自动生成字段,以及对应的访问器逻辑。对属性的赋值也会转移到对字段的赋值。
生成的字段名字不符合c#的规范,但在编译后的文件里是合法的。
在c#中,这样的字段称为匿名字段。我们无法通过名字访问。
属性和方法的区别
属性和方法的区别在于,调用一个方法是可以单独作为一个操作语句的。
但是属性不能单独放置,他仍然会被视作不执行操作的语句。
所以,何时使用属性,何时使用方法,这个问题也有了一个参考标准。
方法可以单独防止,意味着他会做某些事情,会改变什么东西。
而属性只用来获取值,和对自己控制的那个字段进行赋值,不会产生其他影响。
索引器
索引器的名字必须是this
。他的参数使用[]
而不是()
,且必须有至少一个参数。
class StringCollection
{
string[] arr = new string[100];
string this[int i]
{
get => arr[i];
set => arr[i] = value;
}
void Invok()
{
StringCollection sc = new StringCollection();
sc[0] = "Hello";
sc[1] = "World";
Console.WriteLine(sc[0]); //Hello
Console.WriteLine(sc[1]); //World
}
}
索引器的调用方式类似于数组的索引。
索引器同样可以重载,只要参数列表不同。
构造器
声明一个类后,需要调用他的构造器来声明实例。
对于完全没有声明任何构造器的类型,编译器会自动合成一个无参构造器。
C2 c = new C2();
class C2
{
}
主构造器
在类后面直接加括号声明参数,这将成为这个类型的主构造器。
主构造器不能拥有逻辑。
除了构造器以外的成员,都可以访问主构造器里的参数。
如果有属性或方法访问,那么这些参数会作为匿名字段储存。
如果只是作为字段的赋值使用,那么不会储存这些参数。
class Point(int x, int y)
{
int X => x;
int Y => y;
double Length => Math.Sqrt(x * x + y * y);
}
调用构造器时,也需要传入这些参数。
Point p = new Point(3, 4);
当定义了任何一种构造器后,系统就不会自动合成无参构造器了。
次构造器
在类的内部可以额外声明更多的构造器。构造器同样可以进行重载。
构造器没有方法的返回值,方法名和类名一样。
构造器有以下特点。基本上,构造器就是用来做内容的初始化工作的。
- 构造器必定会被调用,且先于其他方法被调用。
- 构造器只会执行一次。
- 可以为只读字段赋值,或者使用
init
访问器。
构造器通常会使用和字段,属性同名的参数。
方法中可以使用this
来访问实例成员。不限于构造器,普通方法和访问器也可以这样。
class Circle
{
double radius;
Circle(double radius)
{
this.radius = radius;
}
double Area => Math.PI * radius * radius;
double Circumference => 2 * Math.PI * radius;
}
构造器链
构造器如果希望调用其他的构造器,则只能在方法执行开始前进行调用。
为此他有特殊的语法。在参数后面加:this()
来调用。
如果类有有参的主构造器,那么次构造器必须使用构造器链直接或间接调用主构造器。
class Point(int x, int y)
{
int X => x;
int Y => y;
double Length => Math.Sqrt(x * x + y * y);
Point(int value) : this(value, value) { }
Point() : this(0) { }
}
终结器
和构造器相反,终结器是在一个对象被清理时触发的。
它由.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;
void Deconstruct(out int x, out int y)
{
x = X;
y = Y;
}
void Deconstruct(out int x, out int y, out double length)
{
(x, y) = this;//像元组一样解构
length = Math.Sqrt(x * x + y + y);
}
}
内部类
类的内部可以再声明类。这称为内部类。
这通常意味着这个类是专属于自己的,用于辅佐自己的东西。
对其他人没有多大用处。
class Barrack
{
int level;
class Marine
{
int Atk;
void Init(Barrack barrack)
{
Atk += barrack.level;
}
}
}
访问权限
默认情况下类成员是不能被外部访问的。如果要公开给外部需要加上访问权限修饰符。
class Point
{
public int X;
public int Y;
}
Point p = new Point();
p.X = 12;
p.Y = 12;
访问权限列表
调用方的位置 | public(公开) | protected internal | protected(保护) | internal(内部) | private protected | private(私有) |
---|---|---|---|---|---|---|
成员和内部类 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
派生类 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
相同程序集 | ✔️ | ✔️ | ❌ | ✔️ | ❌ | ❌ |
不同程序集的派生类 | ✔️ | ✔️ | ✔️ | ❌ | ❌ | ❌ |
任何 | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ |
默认访问权限
成员 | 默认(没写修饰符时)可访问性 | 允许的成员的声明的可访问性 |
---|---|---|
命名空间 | public | 无(不能添加修饰符) |
枚举 | public | 无(不能添加修饰符) |
顶级类 | internal | internal 或 public |
嵌套类 | private | 全部 |
类成员 | private | 全部 |
嵌套结构 | private | 除了带有protected的访问权限。 |
- 尽管构造器默认是给别人调用的。但他的默认访问权限也是私有的。你必须主动添加。
- 属性的访问器可以单独添加访问权限。这样可以声明出只有自己可以修改的属性。
public int Age { get; private set; }
。这也是自动属性的一种用法。
命名约定
具有public
或protected
修饰的所有类成员,都意味着将会被他人使用。
为了代码风格的一致性,约定公开的类成员,以驼峰命名法。
- 使用英文作为名字
- 把所有的空格后的第一个字母大写,然后去掉所有空格
- 把首字母大写。
例如:超光速
- faster than light
- fasterThanLight
- FasterThanLight
不公开的命名规范,没有统一。
封装
为什么要阻止调用者访问所有的东西呢?
- 对于不想知道所有东西的人来说:如果把你带到飞机上的驾驶舱,你看着眼前琳琅满目的按钮你会怎么想?但是,如果我只给你6个键,上下左右前后。有些人可能就敢说会开飞机了。
- 对于想知道所有东西的人来说:不允许外部修改一些关键数据。例如我游戏只做了70关。然后调用者一改,跳关到80关。那我怎么运行呢?
- 对于你自己写的代码而言,你可能觉得没有必要,因为想改就改。但是请记住,真实的开发绝对不会是靠你一个人就能完成的。你可能会下扩展包,可能会用你的同学同事写好的代码。有可能很久以后你想起来你写的一个代码刚好能解决问题,但是已经看不懂你写的具体内容。
职责
在定义类的时候,应该先想好它应该做什么,规划他的职责范围。
在你只有一个地方使用的时候,你可能觉得无所谓,写谁身上都一样。
但是如果东西多起来了,可能就会出现重复的代码。
但是如果把恢复生命的方法,写在人物里面,那么就只需要写一份恢复生命。
而且本来这个操作修改的也是人物自己的东西,恢复生命,本应是人物的职责。
这样,以后不管是谁使用人物,都可以直接使用他的恢复生命,不需要自己写一份了。
实例和静态
这两词难以理解。我们借用一下民法中的术语种类物和特定物。
实例,特定物,是可以说这个
,那个
的东西。
你去商场买电视,在你付钱以后,这个电视
就是属于你的了。
你的东西你想把他当场砸掉都行。
类,种类物,是指整个概念,比如电视这个概念。
你买下的是这个电视
,所以你只能砸这个电视
。
你不能因为你买了电视
+商店里其他电视是电视
就觉得自己可以砸掉商店里其他电视了。
实例成员,是需要构建实例才存在的概念。比如鸭子的重量,必须指定一个鸭子才能讨论。
静态成员,只要有概念存在就可以讨论。比如鸭子是两条腿。不需要讨论哪只鸭子。
静态成员
在一个成员前加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
修饰的命名空间引用,类型别名,静态引用,会作用到这个程序集下的所有文件。文章来源:https://www.toymoban.com/news/detail-437072.html
global using System;
在你的控制台模板项目生成时,就带有一些默认的全局引用。
可以在你的编译器左上角看到他们。
文章来源地址https://www.toymoban.com/news/detail-437072.html
到了这里,关于c#笔记-定义类的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!