WPF-CSharp核心
面向对象
面向过程编程:分析出解决问题所需要的步骤,然后用函数把步骤一步一步实现,使用的时候再一个个依次调用
面向对象编程:用程序来抽象(形容)对象,万物皆对象
面向对象三大核心:
- 封装:用程序语言来形容对象
- 继承:复用封装对象的代码;(儿子继承父亲,复用现成代码)
- 多态:同样行为的不同表现;(儿子继承父亲的基因但是有不同的行为)
封装
class关键字
class
类是对象的模板,可以通过创建类创建对象
a.位置:namespace
b.语法:
1 | class 类名 |
c.规范:
- 类名使用帕斯卡命名法,且不能重复
- 类是引用类型,分配的是堆空间
- 多个类实例之间是完全独立的
d.使用
1 | namespace Progress |
练习:
观察如下代码,说明A等于多少
1
2
3GameObject A = new GameObject();
GameObject B = A;
B = null;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19namespace Progress
{
class GameObject
{
}
class Progress
{
static void Main()
{
GameObject A = new GameObject();
GameObject B = A;
B = null;
Console.WriteLine(A); // Progress.GameObject
}
}
}这一题中,我们要理解
null
的作用。null
表示一个引用类型变量不指向任何对象。它的作用是将引用类型变量的地址设置为空,而不是一个实际的值。- 当你将一个引用类型变量设置为 [
null
](vscode-file://vscode-app/e:/microsoft vs code/resources/app/out/vs/code/electron-sandbox/workbench/workbench-apc-extension.html) 时,例如 [B = null;
](vscode-file://vscode-app/e:/microsoft vs code/resources/app/out/vs/code/electron-sandbox/workbench/workbench-apc-extension.html),你实际上是将栈上的引用地址清空,使其不再指向任何堆上的对象。 - 这意味着变量 [
B
](vscode-file://vscode-app/e:/microsoft vs code/resources/app/out/vs/code/electron-sandbox/workbench/workbench-apc-extension.html) 不再引用任何对象,而不是将某个值赋给B
- 当你将一个引用类型变量设置为 [
观察如下代码,说明A与B是否有关系
1
2
3GameObject A = new GameObject();
GameObject B = A;
B = new GameObject();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19namespace Progress
{
class GameObject
{
}
class Progress
{
static void Main()
{
GameObject A = new GameObject();
GameObject B = A;
B = new GameObject();
Console.WriteLine(Object.ReferenceEquals(A, B)); // false
}
}
}
成员变量与访问修饰符
类中可以有多个成员变量,类型不限,可以有初始值
1 | struct Pet |
和struct
一样,class
也有访问修饰符
public
公共的 内部和外部都可以使用private
私有的 只有内部才能使用,默认值protected
受保护的 只有内部和子类才能访问
实例化一个对象后,会默认赋值(使用default(变量类型)
可查看对应类型的默认赋值)
- 数字类 0
bool
false- 引用类型 null
char
‘’
练习:
定义一个
Person
类,有姓名,身高,年龄,家庭住址等特征,用Person
创建若干个对象1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Person
{
public string name;
public int age;
public float height;
public string address;
}
class Program
{
static void Main()
{
Person p1 = new Person();
Person p2 = null;
Person p3 = new Person();
p3.age = 18;
p3.name = "gcnanmu";
}
}定义一个学生类,有姓名,学号,年龄,同桌等特征,有学习方法。用学生类创建若干个学生
1
2
3
4
5
6
7
8
9
10
11
12class Student
{
public string name;
public int age;
public float height;
public string studentId;
public Student deskmate = null;
public void LearningMethod()
{
Console.WriteLine("记笔记");
}
}定义一个班级类,有专业名称,教师容量,学生等,创建一个班级对象
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
41enum E_Profession
{
ComputerScience,
Software,
IntelligentScience
}
class Student
{
public string name;
public int age;
public float height;
public string studentId;
public Student deskmate = null;
public void LearningMethod()
{
Console.WriteLine("记笔记");
}
}
class Class
{
public E_Profession prof;
public int teacherV;
public Student[] students;
}
class Program
{
static void Main()
{
Class s = new Class();
s.prof = E_Profession.ComputerScience;
s.teacherV = 10;
s.students = [
new Student { name = "张三", age = 18, height = 1.7f, studentId = "001" },
new Student { name = "李四", age = 19, height = 1.8f, studentId = "002" },
new Student { name = "王五", age = 20, height = 1.9f, studentId = "003" }
];
}
}请问
p.age
是多少1
2
3
4
5Person p = new Person();
p.age = 10;
Person p2 = new Person();
p2.age = 20;
Console.WriteLine(p.age);// 10请问
p.age
是多少1
2
3
4
5Person p = new Person();
p.age = 10;
Person p2 = p;
p2.age = 20;
Console.WriteLine(p.age);// 20请问
s.age
是多少1
2
3
4
5Student s = new Student();
s.age = 10;
int age = s.age;
age = 20;
Console.WriteLine(s.age); // 10请问
s.deskmate.age
是多少1
2
3
4
5Student s = new Student();
s.deskmate = new Student();
s.deskmate.age = 10;
Student s2 = s.deskmate;
s2.age = 20;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Student
{
public int age;
public Student deskmate = null;
}
class Program
{
static void Main()
{
Student s = new Student();
s.deskmate = new Student();
s.deskmate.age = 10;
Student s2 = s.deskmate;
s2.age = 20;
Console.WriteLine(s.deskmate.age); // 20
}
}
成员方法
也称成员函数,描述对象要进行的操作
1 | class Person |
稍微复杂的例子
1 | class Person |
练习:
基于成员变量的练习题
为人类定义说话,走路,吃饭等方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class Person
{
public string name;
public int age;
public void Say(string str)
{
Console.WriteLine("{0} say {1}", name, str);
}
public void Walk()
{
Console.WriteLine("正在走路……");
}
public void Eat()
{
Console.WriteLine("正在吃饭……");
}
}基于成员变量的练习题
为学生类定义学习,吃饭等方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Student
{
public string name;
public int age;
public void Study()
{
Console.WriteLine("{0} is studying", name);
}
public void Eat()
{
Console.WriteLine("{0}正在吃饭……", 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
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
63class Person
{
public string name;
public int age;
public void Say(string str)
{
Console.WriteLine("{0} say {1}", name, str);
}
public void Walk()
{
Console.WriteLine("{0}正在走路……", name);
}
public void Eat(Foods f)
{
Console.WriteLine("{0}正在{1}", name, f.name);
}
}
class Student
{
public string name;
public int age;
public void Study()
{
Console.WriteLine("{0} is studying", name);
}
public void Eat(Foods f)
{
Console.WriteLine("{0}正在{1}", name, f.name);
}
}
class Foods
{
public string name;
public int heat;
}
class Program
{
static void Main()
{
Person p = new Person();
p.name = "John";
p.age = 25;
Student s = new Student();
s.name = "gcnanmu";
s.age = 18;
Foods coke = new Foods();
coke.name = "可乐";
coke.heat = 99;
p.Eat(coke);
s.Eat(coke);
}
}
构造函数
实例化对象时进行初始化的函数,即使不写,默认也会存在一个无参构造函数
1 | class Person |
规范:
如果内部实现了有参构造函数且无手动实现无参构造,那么会失去无参构造的特性
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
29class Person
{
// 无参构造函数被注释
// public Person()
// {
// name = "gcnanmu";
// age = 18;
// }
public Person(string name, int age)
{
this.name = name;
this.age = age;
}
public string name;
public int age;
}
class Program
{
static void Main()
{
// 报错:“Person”不包含采用 0 个参数的构造函数
Person p = new Person();
}
}一种特殊写法
构造函数中的冒号
:this()
用于调用同一个类中的另一个构造函数。:this()
会被优先调用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
34class Person
{
public Person()
{
name = "gcnanmu";
age = 18;
}
public Person(string name, int age)
{
this.name = name;
this.age = age;
}
public Person(string name)
{
this.name = name;
}
// 先调用无参构造函数
public Person(int age) : this()
{
this.age = age;
}
public string name;
public int age;
~Person()
{
Console.WriteLine("我被回收了");
}
}:this()
中可以添加参数或常数,会根据参数选择合适的有参构造函数
练习:
写一个Ticket
类,有一个距离变量(在构造对象时赋值,不能为负数),有一个价格特征,有一个方法GetPrice
可以读取到价格,并且根据距离distance
计算price
(1元/公里)
- 0-100公里 不打折
- 101-200公里 打9.5折
- 201-300公里 打9折
- 300公里以上 打8折
有一个显示方法,可以显示这张票的信息。
例如:100公里100块钱
1 | class Ticket |
析构函数与垃圾回收
a.析构函数
当引用类型的堆内存被回收时,会自动调用该函数
语法:
1 | ~类名() |
1 | class Person |
当引用类型的堆内存不再被引用时(null),这片内存就变成了“垃圾”
C#存在垃圾自动回收的机制(GC),几乎不需要使用析构函数进行处理
b.C#垃圾回收机制
垃圾回收(Garbage Collector),简称GC。通过遍历堆上所有动态分配的对象,识别内存是否被引用,来判断当前内存是否需要被释放(垃圾)。
注意点:
- GC只负责堆(Heap)内存的垃圾回收
- 栈(Stack)上的内存是由系统自动分配和释放的,不需要手动管理
- 垃圾回收有很多算法
- 引用计数(Reference Counting)
- 标记清除(Mark Sweep)
- 标记整理(Mark Compact)
- 复制集合(Copy Collection)
C#中的垃圾回收机制的大致原理:
使用分代算法,将堆内存分为三代(0代,1代,2代),内存的速度按顺序由块到慢,容量由小到大。另外,垃圾回收会造成一定的系统开销。
新分配的对象都会先分配到第0代中,每次分配都可能触发垃圾回收和搬迁压缩。触发回收机制后,0代向1代迁移,1代向2代迁移,其余垃圾被释放,在迁移过程中,为了减少内存碎片,会尽量将搬迁的内容存放到连续的内存地址中。随着时间推移,几乎所有的老对象都会被分配到第2代内存中,新的对象都会优先在第0,1代内存中。
大的对象(>84k)会被直接分配到第2代内存中,这是由于垃圾回收需要一定的开销,在一定时间可能会造成卡顿,直接把大对象分配到第2代内存可以规避大的内存空间回收情况出现。
垃圾回收可以在代内存满时被动触发,也可以主动手动触发(GC.Collect()
)
成员属性
作用:
- 保护成员变量
- 为成员变量的获取和赋值添加逻辑
- 解决3大访问修饰符的局限
语法
1 | 访问修饰符 返回值 属性名 |
- 属性命名使用帕斯卡命名法
- 撇除大括号,和函数定义相比少了
()
例子
1 | class Person |
规范
可以在
get
和set
设置访问修饰符,但是访问修饰符的等级要比成员变量低,且不能两个同时使用访问修饰符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
30class Person
{
private string name;
private int money;
public string Name
{
// 报错1:不能为属性或索引器“Person.Name”的两个访问器同时指定可访问性修饰符
protected get
{
return name;
}
private set
{
name = value;
}
}
}
class Progress
{
static void Main()
{
Person p = new Person();
// 报错2:属性或索引器“Person.Name”不能用在此上下文中,因为 set 访问器不可访问(因为设置了private)
p.Name = "gcnanmu";
Console.WriteLine(p.Name);
}
}get
和set
可以只有一个(这种情况下无法使用访问修饰符)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Person
{
private string name;
private int money;
public string Name
{
// 报错:“Person.Name”: 仅当属性或索引器同时具有 get 访问器和 set 访问器时,才能对访问器使用可访问性修饰符
private set
{
name = value;
}
}
}自动属性
特点:无需定义成员变量,自动获得
get
和set
的方法,且此种属性无需额外操作1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class Person
{
private string name;
private int money;
// 之前并没有定义一个height的成员变量
public float Height
{
get;
set;
}
}
class Progress
{
static void Main()
{
Person p = new Person();
p.Height = 1.79f;
Console.WriteLine(p.Height); // 1.79
}
}
练习
定义一个学生类,有五种属性,分别为姓名、年龄、性别、CSharp成绩、Unity成绩
有两个方法:
- 打招呼:介绍自己叫什么,今年几岁了,是男是女
- 计算自己的总分数和平均分并显示
使用属性完成,要求如下:
年龄必须在0-150岁之间
成绩必须是0-100
性别只能是男或女
1 | class Student |
索引器
可以向数组一样访问其中的元素,使程序看起来更直观,更容易编写
语法
1 | 访问修饰符 返回类型 this[参数类型 参数名] |
例子:
1 | class Person |
索引器支持重载
1 | class Person |
1 | class Person |
练习:自定义一个整数类,该类中有一个整数数组变量,为他封装增删改查的方法
静态成员
即使用static
修饰的变量,函数和对象,无需实例化对象也可使用(和常量一样变为了公有)
1 | class Test |
原理:
一旦加上了static
关键字,在程序运行开始时,就会将静态成员分配到静态内存区,因此并非无中生有。另外,静态成员与程序同生共死,几乎不会被GC处理。综上,静态成员具有唯一性和全局性。
内存空间是有限的。因为
static
修饰的静态成员不能被GC,因此如果静态成员足够多,可能会造成其他内存区严重不足,导致程序无法良好运行。
规范:
static
的位置和访问修饰符的前后顺序无关- 静态成员内不能使用非静态成员
- 静态成员可以没有初始值且可以被修改
关于static
与const
:
- 相同点 两者都可以通过
类名.
出来使用 - 不同点
const
必须初始化且不能被修改,而static
没有要求const
必须写在访问修饰符后,而static
没有要求const
只能修饰变量
练习:一个类对象,在整个应用程序的生命周期中,有且经会有一个该对象的存在,不能再外部实例化,直接通过该类名就能得到一个唯一的对象。
1 | class Test |
下一节介绍的静态类能更好题目满足要求
静态类和静态构造函数
使用static
修饰的类和构造函数
作用:
- 静态类: 作为一个工具类,方便使用(如
Console
) - 静态构造函数:用来初始化静态成员(静态函数中只能由静态成员)
静态类的特点:
- 只能包含静态成员
- 不能被实例化
静态构造函数的特点:
不能使用访问修饰符,不能有参数
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
28static class Test
{
// 不能有参数和访问修饰符
static Test()
{
// 为静态变量初始化
age = 18;
name = "gcnanmu";
}
public static int age;
public static string name;
public static void TestFun()
{
Console.WriteLine("testFun");
}
}
class Progress
{
public static void Main(string[] args)
{
Console.WriteLine(Test.name); // gcnanmu
Console.WriteLine(Test.age); // 18
}
}有且只会调用一次
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
36static class Test
{
// 只会调用一次
static Test()
{
// 为静态变量初始化
Console.WriteLine("静态构造函数!");
age = 18;
name = "gcnanmu";
}
public static int age;
public static string name;
public static void TestFun()
{
Console.WriteLine("testFun");
}
}
class Progress
{
public static void Main(string[] args)
{
Console.WriteLine(Test.name);
Console.WriteLine(Test.age);
Test.TestFun();
/*
静态构造函数!
gcnanmu
18
testFun
*/
}
}静态类和非静态都可以有
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
43class Test
{
// 即便生成了两个实例,也只会调用一次
static Test()
{
// 为静态变量初始化
Console.WriteLine("静态构造函数!");
age = 18;
name = "gcnanmu";
}
// 注意:此处并非函数的重载
public Test()
{
// 对象被实例化的时候会调用
Console.WriteLine("普通的构造函数!");
age = 18;
name = "gcnanmu";
}
public static int age;
public static string name;
}
class Progress
{
public static void Main(string[] args)
{
Test ts = new Test();
Test ts2 = new Test();
Console.WriteLine(Test.name);
Console.WriteLine(Test.age);
/*
静态构造函数!
普通的构造函数!
普通的构造函数!
gcnanmu
18
*/
}
}
练习:写一个用于数学计算的静态类;该类提供计算圆的面积,圆的周长,矩形面积,矩形周长,取一个树的绝对值等方法
1 | static class Calculate |
扩展方法
为现有的非静态类型添加新的方法
作用:
- 在不修改原始类的条件下,提升程序的扩展性
- 不需要通过继承来添加方法
特点:
- 只能写在静态类中
- 一定是一个静态函数,第一参数为扩展目标且必须使用this修饰
- 当函数名同名时,如果不触发重载,那么不会修改原有函数的功能
语法:
1 | static class Tools |
为现有的类添加方法
1 | static class Tools |
为自定义的类添加方法
1 | static class Tools |
练习:
为整形扩展一个求平方的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19static class Tools
{
public static int Pow2(this int i)
{
return i * i;
}
}
class Progress
{
public static void Main(string[] args)
{
int i = 2;
i = i.Pow2();
Console.WriteLine(i); // 4
i = i.Pow2();
Console.WriteLine(i); // 16
}
}写一个玩家类,包含姓名、血量、攻击力、防御力等特征,攻击、移动、受伤等方法
为玩家扩展一个自杀的方法
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
54static class Tools
{
public static void Suicide(this Player p)
{
p.hp = 0;
Console.WriteLine("玩家自杀了");
}
}
class Player
{
private static Random r = new Random();
public Player(string name)
{
this.name = name;
}
public string name;
public int hp = 100;
public int hack = r.Next(1, 101);
public int defense = r.Next(1, 100);
public void Move()
{
Console.WriteLine("{0}正在移动", name);
}
public void Attack()
{
Console.WriteLine("{0}正在攻击", name);
}
public void wounded(int h)
{
if (defense < h)
{
hp -= h;
}
Console.WriteLine("{0}受伤", name);
}
}
class Progress
{
public static void Main(string[] args)
{
Player p = new("gcnanmu");
p.Suicide(); // 玩家自杀了
Console.WriteLine(p.hp); // 0
}
}
运算符重载
让自定义类或结构体的对象可以进行运算操作
语法
1 | public static 返回值 operator 运算符(参数列表){} |
例子
1 | class Point |
规范:
参数中必有一个参数为当前的对象名,返回值类型随意,必须为静态方法
一个符号支持多个重载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class Point
{
public int x;
public int y;
// 重载+号 实现对象相加
public static Point operator +(Point p1, Point p2)
{
Point p = new Point();
p.x = p1.x + p2.x;
p.y = p1.y + p2.y;
return p;
}
// 支持重载
public static Point operator +(Point p1, int value)
{
Point p = new Point();
p.x = p1.x + value;
p.y = p1.y + value;
return p;
}
}各运算符所需参数不同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class Point
{
public int x;
public int y;
// 重载+号 必须有两个参数
public static Point operator +(Point p1, Point p2)
{
Point p = new Point();
p.x = p1.x + p2.x;
p.y = p1.y + p2.y;
return p;
}
// 重载! 有且只能有一个参数
public static bool operator !(Point p)
{
return false;
}
}运算符的重载必须成对实现 (实现了
>
,就必须实现<
;实现了==
,必须是西安!=
;实现了>=
,必须实现<=
)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class Point
{
public int x;
public int y;
// 运算符“Point.operator >(Point, int)”要求也要定义匹配的运算符“<”
public static Point operator >(Point p1, int value)
{
return null;
}
// 运算符“Point.operator ==(Point, int)”要求也要定义匹配的运算符“!=”
public static Point operator ==(Point p1, int value)
{
return null;
}
// 运算符“Point.operator >=(Point, int)”要求也要定义匹配的运算符“<=”
public static Point operator >=(Point p1, int value)
{
return null;
}
}不可重载的运算符
- 逻辑与和或:
&&
、||
- 索引符:
[]
- 强制运算符:
()
- 点:
.
- 三目运算符:
?:
- 赋值运算符:
=
- 逻辑与和或:
练习:
定义一个位置结构的类,为其重载判断是否相等的运算符(x1,y1)==(x2,y2)=> 两个值相等时才为true
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
58class Point
{
public int x;
public int y;
public Point(int x, int y)
{
this.x = x;
this.y = y;
}
public Point()
{
}
// 重载+号 实现对象相加
public static bool operator ==(Point p1, Point p2)
{
if ((p1.x == p2.x) && (p1.y == p2.y))
{
return true;
}
return false;
}
public static bool operator !=(Point p1, Point p2)
{
if ((p1.x != p2.x) || (p1.y != p2.y))
{
return true;
}
return false;
}
}
class Progress
{
public static void Main(string[] args)
{
Point p = new Point(x: 1, y: 1);
Point p2 = new Point();
p2.x = 2;
p2.y = 2;
Point p3 = new Point(x: 2, y: 3);
Point p4 = new Point(x: 2, y: 2);
Console.WriteLine("{0}", p == p2); // False
Console.WriteLine("{0}", p2 != p3); // True
Console.WriteLine("{0}", p2 == p4); // True
}
}定义一个Vector3类(x,y,z)通过重载运算符实现以下运算
- (x1,y1,z1)+ (x2,y2,z2) = (x1+x2,y1+y2,z1+z2)
- (x1,y1,z1)-(x2,y2,z2)= (x1-x2,y1-y2,z1-z2)
- (x1,y1,z1)* num = (x1 * num,y1 * num,z1 * num)
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
72class Point
{
public int x;
public int y;
public int z;
public Point(int x, int y, int z)
{
this.x = x;
this.y = y;
this.z = z;
}
public Point()
{
}
// 重载+号 实现对象相加
public static Point operator +(Point p1, Point p2)
{
Point p = new Point();
p.x = p1.x + p2.x;
p.y = p1.y + p2.y;
p.z = p1.z + p2.z;
return p;
}
public static Point operator -(Point p1, Point p2)
{
Point p = new Point();
p.x = p1.x - p2.x;
p.y = p1.y - p2.y;
p.z = p1.z - p2.z;
return p;
}
public static Point operator *(Point p1, int value)
{
Point p = new Point();
p.x = p1.x * value;
p.y = p1.y * value;
p.z = p1.z * value;
return p;
}
public void PrintXYZ()
{
Console.WriteLine("x:{0} y:{1} z:{2}", x, y, z);
}
}
class Progress
{
public static void Main(string[] args)
{
Point p1 = new Point(x: 1, y: 1, z: 1);
Point p2 = new Point(x: 2, y: 2, z: 2);
(p1 * 2).PrintXYZ();
(p1 + p2).PrintXYZ();
(p1 - p2).PrintXYZ();
/*
x:2 y:2 z:2
x:3 y:3 z:3
x:-1 y:-1 z:-1
*/
}
}
内部类和分部类
内部类:在类中再申明一个类,强调的是两类的亲密程度
特点:需要使用.运算访问,且受到访问修饰符的制约
1 | class Person |
分部类:使用partial
将类的实现分成多个部分(还是一个整体)
特点:
- 分部类可以写在多个脚本文件中
- 分部类的访问修饰符要一致
- 分部类不能有重复成员
分部方法:将方法的实现和申明进行分离
- 不能加访问修饰符,默认私有
- 只能在分部类中申明
- 返回值类型只能是void
- 可以有参数但不用out关键字
1 | // 如果要加访问修饰符 上下分部类必须一致 |
继承
基础概念和语法
一个类B继承另一个类A,B会获得A类允许继承的所有成员、方法、对象。其中A称为父类,B称为子类,子类B也可以在A的基础上添加自己所特有的逻辑。
特点:
- 单根性:子类只能拥有一个父类
- 传递性:子类可以继承父类的父类
- 受到访问修饰符的影响
语法:class 子类: 父类 {}

1 | class Teacher |
子类中可以出现与父类相同的成员,这样会将父类的成员覆盖,失去继承的目的,因此不推荐这样做
练习:写一个人类,人类中有姓名,有说话的行为,战士类继承人类,有攻击行为
1 | class Person |
里氏替换原则
任何父类出现的地方,子类都可以替代,即父类装子类对象(因为子类包含父类的所有内容),可以方便进行对象存储和管理
1 | class GameObject |
常用于用父类数组装载子类对象
1 | class Progress |
常搭配is
与as
关键字使用
1 | class GameObject |
is
关键字:判断当前对象是否属于某个类,属于时返回true
,反之false
as
关键字:将当前对象转化类型,只支持引用类型相同的转化。如果成功,返回转化对象,反之返回null
另外,还可以通过
()
进行转化
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 for (var i = 0; i < objects.Length; i++)
{
if (objects[i] is Player)
{
Player player = (Player)objects[i];
player.Move();
}
else if (objects[i] is Enemy)
{
Enemy enemy = (Enemy)objects[i];
enemy.Move();
}
}
练习:
1. 写一个Monster类,它派生出Boss和Gobin这两个类,Boss有技能;小怪有攻击;随机生成十个怪,装载到数组中,遍历这个数组,调用他们的攻击方法,如果是Boss就释放攻击
```C
class Monster
{
protected Random r = new Random();
}
class Boss : Monster
{
public void Skill()
{
int damage = r.Next(1, 10);
Console.WriteLine("Boss attacked and dealt " + damage + " damage.");
}
}
class Gobin : Monster
{
public void Attack()
{
int damage = r.Next(1, 5);
Console.WriteLine("Gobin attacked and dealt " + damage + " damage.");
}
}
class Program
{
public static void Main(string[] args)
{
Monster[] monsters = new Monster[10];
Random r = new Random();
for (var i = 0; i < 10; i++)
{
if (r.Next(1, 10) % 2 == 0)
{
monsters[i] = new Boss();
}
else
{
monsters[i] = new Gobin();
}
}
foreach (var monster in monsters)
{
if (monster is Boss)
{
((Boss)monster).Skill();
}
else if (monster is Gobin)
{
((Gobin)monster).Attack();
}
}
}
}
FPS游戏模拟
写一个玩家类,玩家可以拥有武器,现在有冲锋枪,霰弹枪,手枪,匕首四种武器,玩家默认有匕首,请在玩家类中写一个方法,可以拾取不同的武器替换自己拥有的枪械
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
class Player
{
public Programs.EWeapons weapons = Programs.EWeapons.Knife;
public void ChangeWeapon(Programs.EWeapons weapon)
{
weapons = weapon;
}
}
class Programs
{
public enum EWeapons
{
/// <summary>
/// 匕首
/// </summary>
Knife,
/// <summary>
/// 霰弹枪
/// </summary>
Shotgun,
/// <summary>
/// 手枪
/// </summary>
Pistol,
}
public static void Main(string[] args)
{
Player player = new Player();
Console.WriteLine(player.weapons);
player.ChangeWeapon(EWeapons.Shotgun);
Console.WriteLine(player.weapons);
}
}
继承中的构造函数
继承类也可以有构造函数,构造函数的执行存在由上往下的特点
1 | class Father |
规范:
着重注意父类的无参构造。这是因为在创建子类对象时,必然会触发父类的构造函数,因此必须保证父类的构造函数能被调用
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
27class Father
{
// public Father()
// {
// Console.WriteLine("Father default constructor");
// }
public Father(int a, int b)
{
Console.WriteLine("Father 2 parameter constructor");
}
}
class Child : Father
{
// error: 未提供与“Father.Father(int, int)”的所需参数“a”对应的参数
public Child()
{
Console.WriteLine("Child default constructor");
}
// error: 未提供与“Father.Father(int, int)”的所需参数“a”对应的参数
public Child(int a, int b)
{
Console.WriteLine("Child 2 parameter constructor");
}
}子类可以通过
base
关键字调用父类的无参构造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
28class Father
{
public Father()
{
Console.WriteLine("Father default constructor");
}
public Father(int a, int b)
{
Console.WriteLine("Father 2 parameter constructor");
}
}
class Child : Father
{
// 调用父类的无参构造
public Child()
{
Console.WriteLine("Child default constructor");
}
// 调用父类的有参构造
public Child(int a, int b) : base(a, b)
{
Console.WriteLine("Child 2 parameter constructor");
}
}
作业:有一个打工人基类,有工种,工作内容两个特征,一个工作方法;程序员,策划,美术分别继承打工人;
请使用继承中的构造函数实例化3个对象,分别是程序员,策划,美术
1 | class Workers |
万物之父与装箱与拆箱
关键字object
,是用来装载梭有类型的基类(引用类型)
大写
Object
与小写Object
是等价的,object
是Object
的别名
作用:
可以利用里氏替换原则,用
object
容器装所有对象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
28class Father
{
}
class Son : Father
{
}
class Program
{
public static void Main(string[] args)
{
// object替换值类型
object s = new Son();
object i = 1;
int iInt = (int)i;
// object替换引用类型
object arr = new object[10];
object str = "123456";
string strString2 = str as string;
string strString3 = str.ToString();
}
}引用类型推荐使用
as
进行类型转化可以用来表示不确定类型,作为函数参数类型
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
44class Father
{
}
class Son : Father
{
}
class Program
{
public static void Main(string[] args)
{
// object作为函数参数
Function("123", 123, new Father());
/*
123 is string
123 is int
Father is Father
*/
}
public static void Function(params Object[] arr)
{
foreach (var item in arr)
{
if (item is string)
{
Console.WriteLine("{0} is string", item);
}
else if (item is int)
{
Console.WriteLine("{0} is int", item);
}
else if (item is Father)
{
Console.WriteLine("{0} is Father", item);
}
}
}
}
装箱与拆箱
- 装箱:将值类型用
object
存储,从栈空间转移到堆空间 - 拆箱:将引用类型转化为值类型,从堆空间转移到栈空间
好处:方便参数的存储和传递
坏处:存在内存迁移,增加了性能消耗(尽量少用)
密封类
无法被继承的类
语法:在class
使用sealed
关键字
主要作用:不允许该类被继承,保证程序的规范性和安全性
1 | sealed class Father{ |
练习:定义一个载具类,速度、最大速度、可乘人数、司机和乘客等,有上车、下车、行驶和车祸等方法,用载具类申明一个对象,并将若干人装载上车
1 | class Vehicle |
多态
vob
让同一类型的对象,执行相同行为时有不同的表现,保证同一对象有不同的行为表现
vob是指三个关键字:virtual
、override
、base
virtual
将函数变为虚函数 支持在子类重写该函数override
配对virtual
来实现函数重写base
代指父类
以之前学习继承为例
1 | class Father |
以上的例子,使用父类来装载子类,结果是一个对象呈现出两种不同的方法,这违背了面向对象的初衷。
使用virtual
和override
来解决这个问题,让同一对象有唯一的行为特征
1 | class Father |
想要保留父类中的方法,可以使用
base
进行调用
练习
真的鸭子嘎嘎叫,木头鸭子吱吱叫,橡皮鸭子唧唧叫
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
40class Duck
{
public virtual void Call()
{
Console.WriteLine("嘎嘎");
}
}
class WoodDuck : Duck
{
public override void Call()
{
// base.Call();
Console.WriteLine("吱吱");
}
}
class RubberDuck : Duck
{
public override void Call()
{
// base.Call();
Console.WriteLine("唧唧");
}
}
class Program
{
public static void Main(string[] args)
{
Duck d = new Duck();
d.Call();
Duck wd = new WoodDuck();
wd.Call();
Duck rd = new RubberDuck();
rd.Call();
}
}所有员工九点打卡,经理十一点打卡,程序员不打卡
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
39class Employee
{
public virtual void Clock()
{
Console.WriteLine("九点打卡");
}
}
class Manager : Employee
{
public override void Clock()
{
// base.Clock();
Console.WriteLine("十一点打卡");
}
}
class Programer : Employee
{
public override void Clock()
{
// base.Clock();
}
}
class Program
{
public static void Main(string[] args)
{
Employee m = new Employee();
m.Clock();
Employee ma = new Manager();
ma.Clock();
Employee pr = new Programer();
pr.Clock();
}
}创建一个圆形类,有求周长与面积的两个方法
创建矩形类,正方形类,图形继承面积类,实例化矩形,正方形,圆形对象求面积和周长
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
59class Graphics
{
public virtual float Area(params int[] arr)
{
return 0;
}
public virtual float Circumference(params int[] arr)
{
return 0;
}
}
class Rectangle : Graphics
{
public override float Area(params int[] arr)
{
// return base.Area(arr);
return arr[1] * arr[0];
}
public override float Circumference(params int[] arr)
{
return (arr[1] + arr[0]) * 2;
}
}
class Square : Graphics
{
public override float Area(params int[] arr)
{
return arr[0] * arr[0];
}
public override float Circumference(params int[] arr)
{
return arr[0] * 4;
}
}
class Program
{
public static void Main(string[] args)
{
Graphics g = new Graphics();
Console.WriteLine(g.Area());
Console.WriteLine(g.Circumference());
Graphics r = new Rectangle();
Console.WriteLine(r.Area(1, 2));
Console.WriteLine(r.Circumference(1, 2));
Graphics s = new Square();
Console.WriteLine(s.Area(2));
Console.WriteLine(s.Circumference(2));
}
}
抽象类和抽象方法
指的是被abstract
修饰的类和方法
特点:
- 抽象类:无法被实例化
- 抽象方法:
- 只能在抽象类中定义
- 没有方法体
- 必须在继承类中重写,因此不能是私有的
1 | abstract class Thing |
作用:适用于不希望对象被实例化,或者父类的行为不需要被实现,只希望子类去实现定义具体的规则的情况
练习:
写一个动物抽象类,写三个子类 人叫 狗叫 猫叫
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
28abstract class Animal
{
public abstract void Speak();
}
class People : Animal
{
public override void Speak()
{
Console.WriteLine("人叫");
}
}
class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("狗叫");
}
}
class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("猫叫");
}
}创建一个圆形类,有求周长与面积的两个方法
创建矩形类,正方形类,图形继承面积类,实例化矩形,正方形,圆形对象求面积和周长
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
47abstract class Graphics
{
public abstract float Area(params int[] arr);
public abstract float Circumference(params int[] arr);
}
class Rectangle : Graphics
{
public override float Area(params int[] arr)
{
// return base.Area(arr);
return arr[1] * arr[0];
}
public override float Circumference(params int[] arr)
{
return (arr[1] + arr[0]) * 2;
}
}
class Square : Graphics
{
public override float Area(params int[] arr)
{
return arr[0] * arr[0];
}
public override float Circumference(params int[] arr)
{
return arr[0] * 4;
}
}
class Program
{
public static void Main(string[] args)
{
Graphics r = new Rectangle();
Console.WriteLine(r.Area(1, 2));
Console.WriteLine(r.Circumference(1, 2));
Graphics s = new Square();
Console.WriteLine(s.Area(2));
Console.WriteLine(s.Circumference(2));
}
}
接口
接口是行为的抽象规范,也是一种自定义的类型
语法
1 | interface 接口名 |
申明规范
- 不包含成员变量,只包含方法、属性、索引器、事件
- 成员不可以被实现
- 成员可以不写访问修饰符(默认是
public
),不能写private
- 接口不能继承类,但可以继承接口
1 | interface IFly |
使用规范
- 类可以继承多个接口
- 类继承接口后,必须实现其中所有的成员
- 接口继承接口无需实现接口方法(相当于接口的合并)
- 接口是用来继承的,不能被实例化,但可以使用里氏替换原则作为容器
1 | interface IAttack |
显示实现接口:适用于两个接口中有着同名方法的情况
1 | interface IAttack |
支持在继承类中使用virtual
让子类重写接口的方法
1 | interface IAttack |
练习
人、汽车、房子都需要登记;人需要到派出所登记,汽车需要去车管所登记,房子需要去房管局登记
使用接口实现登记方法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
30interface ISignIn
{
void SignIn();
}
class Person : ISignIn
{
public void SignIn()
{
Console.WriteLine("派出所登记");
}
}
class Car : ISignIn
{
public void SignIn()
{
Console.WriteLine("车管所登记");
}
}
class House : ISignIn
{
public void SignIn()
{
Console.WriteLine("房管局登记");
}
}麻雀、 驼鸟、 企鹅、 鹦鹉、直升机、 天鹅
直升机和部分鸟能飞
驼鸟和企鹅不能飞
企鹅和天鹅能游泳
除直升机,其它都能走
请用面向对象相关知识实现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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105interface IFly
{
void Fly();
}
interface ISwim
{
void Swim();
}
interface IWalk
{
void Walk();
}
/// <summary>
/// 直升飞机
/// </summary>
class Helicopter : IFly
{
public void Fly()
{
Console.WriteLine("Helicopter is flying");
}
}
/// <summary>
/// 企鹅
/// </summary>
class Penguin : ISwim, IWalk
{
public void Swim()
{
Console.WriteLine("Penguin is swimming");
}
public void Walk()
{
Console.WriteLine("Penguin is walking");
}
}
/// <summary>
/// 天鹅
/// </summary>
class Swan : IFly, ISwim, IWalk
{
public void Fly()
{
Console.WriteLine("Swan is flying");
}
public void Swim()
{
Console.WriteLine("Swan is swimming");
}
public void Walk()
{
Console.WriteLine("Swan is walking");
}
}
/// <summary>
/// 麻雀
/// </summary>
class Sparrow : IFly, IWalk
{
public void Fly()
{
Console.WriteLine("Sparrow is flying");
}
public void Walk()
{
Console.WriteLine("Sparrow is walking");
}
}
/// <summary>
/// 鸵鸟
/// </summary>
class Ostrich : IWalk
{
public void Walk()
{
Console.WriteLine("Ostrich is walking");
}
}
/// <summary>
/// 鹦鹉
/// </summary>
class Parrot : IFly, IWalk
{
public void Fly()
{
Console.WriteLine("Parrot is flying");
}
public void Walk()
{
Console.WriteLine("Parrot is walking");
}
}个人理解:由于部分鸟不能飞,因此不能创建一个名为
bird
的类并继承了IFly
多态来模拟移动硬盘、U盘、MP3插到电脑上读取数据
移动硬盘与U盘都属于存储设备
MP3属于播放设备
但他们都能插在电脑上传输数据
电脑提供了一个USB接口
请实现电脑的传输数据的功能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
80
81interface IUSB
{
void Write(string s);
string Read();
}
abstract class StoreDevices : IUSB
{
public string name;
public abstract void Write(string s);
public abstract string Read();
}
abstract class PlayerDevices : IUSB
{
public string name;
public abstract void Write(string s);
public abstract string Read();
}
class UCD : StoreDevices
{
public override void Write(string s)
{
Console.WriteLine("UCD Write: " + s);
}
public override string Read()
{
return "UCD Read";
}
}
class SataSSD : StoreDevices
{
public override void Write(string s)
{
Console.WriteLine("SataSSD Write: " + s);
}
public override string Read()
{
return "SataSSD Read";
}
}
class MP3 : PlayerDevices
{
public override void Write(string s)
{
Console.WriteLine("MP3 Write: " + s);
}
public override string Read()
{
return "MP3 Read";
}
}
class Computer
{
public void TransferData(IUSB device)
{
device.Write("Hello");
Console.WriteLine(device.Read());
}
}
class Program
{
static void Main(string[] args)
{
Computer computer = new Computer();
computer.TransferData(new UCD());
computer.TransferData(new SataSSD());
computer.TransferData(new MP3());
}
}
密封方法
用sealed
关键字修饰的重写函数,和override
配合使用
作用:让虚方法和抽象方法之后不能被重写
1 | abstract class Father |
命名空间
是用来组织和重用代码的
作用:类似于一个工具包,类就像是一件件的工具
语法
1 | namespace 空间名 |
使用:
支持分离式的写法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16namespace Thing
{
class Person
{
}
}
// 实际上都同属于一个Thing
namespace Thing
{
class Boss
{
}
}不同的命名空间相互使用
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
32using Thing;
namespace Thing
{
class Person
{
}
}
namespace Thing
{
class Boss
{
}
}
namespace Program
{
class Program
{
static void Main(string[] args)
{
// 用法一:指明出处
Thing.Person person = new Thing.Person();
// 用法二:引用当前命名空间
Person person2 = new Person();
}
}
}不同命名空间允许有相同类名
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
28namespace Demo1
{
class Person
{
}
}
namespace Demo2
{
class Person
{
}
}
namespace Program
{
class Program
{
static void Main(string[] args)
{
// 只能使用指明出处的方法访问
Demo1.Person p1 = new Demo1.Person();
Demo2.Person p2 = new Demo2.Person();
}
}
}命名空间可以包裹命名空间
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
28using GameObject.Player;
namespace GameObject
{
namespace Player
{
class Player
{
public void Move() { }
}
}
}
namespace Program
{
class Program
{
static void Main(string[] args)
{
// 方式1 指明出处
GameObject.Player.Player player = new GameObject.Player.Player();
player.Move();
// 方式2 使用using
Player player2 = new Player();
}
}
}访问修饰符
internal
:namespace
中的类默认为internal
修饰,指的是只能在当前程序集中使用。以
Visual Studio
为例,一个解决方案下支持创建多个项目,一个解决方案就是一个程序集,项目之间也支持相互调用(但不支持使用private
修饰namespace
中的类)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23namespace UI
{
// internal:指的是只有在同一程序集中才能访问
internal class Image
{
public Image()
{
Console.WriteLine("UI Image");
}
}
}
namespace Program
{
class Program
{
static void Main(string[] args)
{
UI.Image image1 = new UI.Image(); // UI Image
}
}
}
练习:有两个命名空间, UI(用户界面)和Graph (图表),两个命名空问中都有一个lmage 类,请在主函数中实例化两个 不同命名空间中的lmage对象
1 | namespace UI |
万物之父中的方法
C#中Object
源码部分如下(删除了部分的注释)
1 | public partial class Object |
除了无参构造函数和析构函数,剩下的成员简单分类为:
- 静态方法
ReferenceEquals
、Equals
- 成员方法
GetType
、MemberwiseClone
- 虚函数
ToString
、Equals
、GetHashCode
静态方法
Equals
用于比较值类型是否相等,返回bool
值
ReferenceEquals
用于比较引用类型是否相等,返回bool
值 如果传入两个值类型 返回False
1 | class Thing |
成员方法
GetType
获取当前成员变量的类型 返回值为Type
MemberwiseClone
(protected) 对对象进行浅克隆 返回类型为object
1 |
|
虚函数
以下函数支持重写
ToString
默认返回当前对象所属类的命名空间+类名
(string)
Equals
默认为ReferenceEquals
效果,比较两个对象的引用是否相等 返回bool
值
GetHashCode
获取当前变量的Hash值(变量的唯一标识符)(不演示)
1 | namespace Program |
支持自定义Equals
和ToString
的规则
1 | namespace Program |
练习
有一个玩家类,有姓名,血量,攻击力,防御力,闪避率等特征
请在控制台打印出 “玩家XX,血量XX,攻击力XX,防御力XX” XX为具体内容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
50static class Tool
{
public static int RandomInt(int min, int max)
{
return new Random().Next(min, max);
}
// 生成0-1的百分比
public static float RandomFloat()
{
return (float)new Random().NextDouble();
}
}
class Player
{
public string Name { get; set; }
public int hp = 100;
public int attack = Tool.RandomInt(5, 10);
public int defense = Tool.RandomInt(2, 5);
public float missRate = Tool.RandomFloat();
public override string ToString()
{
// return base.ToString();
return $"{Name} has {hp} hp, {attack} attack, {defense} defense, {missRate} miss rate";
}
}
class Program
{
public static void Main(string[] args)
{
Player player = new Player();
player.Name = "Player1";
Player player2 = new Player();
player2.Name = "Player2";
Console.WriteLine(player);
Console.WriteLine(player2);
/*
Player1 has 100 hp, 5 attack, 4 defense, 0.56745094 miss rate
Player2 has 100 hp, 9 attack, 2 defense, 0.87566304 miss rate
*/
}
}一个Monster类的引用对象A, Monster类有攻击力、防御力、血量、技能ID等属性。我想复制一个和A对象一模一样的B对象。并且改变了B的属性,A是否会受到影响?请问如何买现?
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
42class Monster
{
public int attack;
public int defense;
public int health = 100;
public int skillCD = 0;
public Monster Clone()
{
return (Monster)this.MemberwiseClone();
}
public override string ToString()
{
// return base.ToString();
return "Monster: " + attack + " " + defense;
}
}
class Program
{
public static void Main(string[] args)
{
Monster monster = new Monster();
monster.attack = 10;
monster.defense = 5;
Monster monster1 = monster.Clone();
monster1.attack = 20;
monster1.defense = 10;
Console.WriteLine(monster);
Console.WriteLine(monster1);
/*
Monster: 10 5
Monster: 20 10
*/
}
}从结果来看,A和B并不会相互影响
string
本小节补充string
常用的方法
字符串的本质是数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14string str = "小市民";
// 支持索引访问其中的元素
for (var i = 0; i < str.Length; i++)
{
Console.WriteLine(str[i]);
}
// 支持转化为char数组
char[] chars = str.ToCharArray();
foreach (var c in chars)
{
Console.WriteLine(c);
}字符串拼接
1
2
3
4
5
6
7string str = "小市民";
string result = string.Format("{0}是一个{1}。", "小明", str);
Console.WriteLine(result); // 小明是一个小市民。
string result2 = $"小明是一个{str}。";
Console.WriteLine(result2); // 小明是一个小市民。正向查找字符位置
1
2
3
4
5
6
7
8
9string str = "小市民";
// 返回第一个匹配字符的索引
int index = str.IndexOf("市");
Console.WriteLine(index); // 1
// 如果没有找到匹配字符,则返回 -1。
index = str.IndexOf("w");
Console.WriteLine(index); // -1反向查找字符位置
1
2
3
4
5
6
7
8
9string str = "小市民小市民";
// 返回第一个匹配字符的索引
int index = str.IndexOf("小市民");
Console.WriteLine(index); // 0
// 返回最后一个匹配字符的索引
index = str.LastIndexOf("小市民");
Console.WriteLine(index); // 3移除指定位置后的字符
1
2
3
4
5
6
7
8
9
10
11string str = "小市民小市民";
// 将字符串中指定位置的字符删除,返回新的字符串。
// 一个参数:从指定位置开始删除到字符串末尾(包括这个位置)
string str2 = str.Remove(3);
Console.WriteLine(str2); // 小市民
// 两个参数:从指定位置开始删除指定长度的字符。
string str3 = str.Remove(1, 2);
Console.WriteLine(str3); // 小小市民替换指定字符串
1
2
3
4
5
6
7
8string str = "小市民小市民";
string str2 = str.Replace("小市民", "大富豪");
Console.WriteLine(str2); // 大富豪大富豪
// 清空字符串
string str3 = str.Replace("小市民", "");
Console.WriteLine(str3); //大小写转换
1
2
3
4
5
6
7
8string name1 = "gcnanmu";
string name2 = "GCNANMU";
string str1 = name1.ToUpper();
Console.WriteLine(str1); // GCNANMU
string str2 = name2.ToLower();
Console.WriteLine(str2); // gcnanmu字符串截取
1
2
3
4
5
6
7
8
9
10
11string str = "小市民小市民";
// 从字符串中提取子字符串 从索引3开始提取到结尾
string str2 = str.Substring(3);
// 从字符串中提取子字符串 从索引0开始提取三个字符 不能超过字符串长度
string str1 = str.Substring(0, 3);
string str3 = str.Substring(0, 10); // error:会抛出异常
Console.WriteLine(str1); // 小市民
Console.WriteLine(str2); // 小市民字符串切割
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15string str = "小市民-日文-轻文学-米泽穗信";
string[] arr = str.Split('-');
foreach (string s in arr)
{
Console.WriteLine(s);
}
/*
小市民
日文
轻文学
米泽穗信
*/
练习
请将字符串
1|2|3|4|5|6|7
变为2|3|4|5|6|7|8
1
2
3
4
5
6
7
8
9
10
11
12string str = "1|2|3|4|5|6|7";
string[] arr = str.Split("|");
string result = "";
for (var i = 1; i < arr.Length; i++)
{
result += arr[i] + "|";
}
result += "8";
Console.WriteLine(result); // 2|3|4|5|6|7|8string str = null; str = "123"; string str2 = str; str2 = "321"; str2 += "123";
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
请说明上述代码分配了多少个堆空间
- `str = "123"`分配了一次堆空间
- `str2 = "321"`分配了一次堆空间
- `str2 += "123"` 创建了一个新的实例,并将地址赋值给`str2`
3. 编写一个函数,将输入的字符串反转,不要使用中间商,你必须原地修改输入的数组,交换过程不适用额外的空间
如:输入:`{'h','e','l','l','o'}` 输出:`{'o','l','l','e','h'}`
```C#
class Program
{
static void ReverseString(char[] str)
{
int left = 0;
int right = str.Length - 1;
while (left < right)
{
// 异或两次的结果为自身
/*
假设 str[left] = a str[right] = b
a = a ^ b
b = b ^ a ^ b = a
a = a ^ b ^ a = b
*/
str[left] ^= str[right];
str[right] ^= str[left];
str[left] ^= str[right];
left++;
right--;
}
}
static void Main(string[] args)
{
char[] str = "Hello".ToCharArray();
ReverseString(str);
Console.WriteLine(new string(str)); // 输出: "olleH"
}
}
stringBuilder
从上小节的练习能够知道,字符串只要进行赋值、改变(增删改查)都会产生新的堆空间,如果操作频繁会产生大量的内存垃圾,更频繁的触发GC,造成不必要的性能开销。StringBuilder
可以很好解决这个问题
1 | using System.Text; |
使用StringBuilder
需要引用System.Text
的命名空间,可以看到StringBuilder
预先扩大了字符串的容量,减少因为空间变化造成的内存开辟和迁移,明显降低垃圾的产生速度。根据字符串长度的变化,会自动进行扩容(默认为当前大小乘以2)。
StringBuilder
操作和string
类有明显的不同
增加
1
2
3
4
5
6
7
8
9using System.Text;
StringBuilder str = new StringBuilder("小市民");
str.AppendFormat("{0}", 123);
str.Append(456);
Console.WriteLine(str); // 小市民123456插入
1
2
3
4
5
6
7
8using System.Text;
StringBuilder str = new StringBuilder("小市民");
str.Insert(0,"我是");
Console.WriteLine(str); // 我是小市民查找
1
2
3
4
5
6
7
8using System.Text;
StringBuilder str = new StringBuilder("小市民");
Console.WriteLine(str[0]); // 小
Console.WriteLine(str[1]); // 市
Console.WriteLine(str[2]); // 民修改
1
2
3
4
5
6
7
8using System.Text;
StringBuilder str = new StringBuilder("小市民");
// 通过索引器访问字符串中的字符 只能是char
str[0] = '大';
Console.WriteLine(str); // 大市民删除
1
2
3
4
5
6
7using System.Text;
StringBuilder str = new StringBuilder("小市民");
// 从索引 0 开始删除 1 个字符
str.Remove(0, 1);
Console.WriteLine(str); // 输出:市民清空
1
2
3
4
5
6using System.Text;
StringBuilder str = new StringBuilder("小市民");
str.Clear();
Console.WriteLine(str); // 输出:替换
1
2
3
4
5
6
7
8using System.Text;
StringBuilder str = new StringBuilder("小市民");
// 从索引 0 开始删除 1 个字符
str.Replace("小", "大");
Console.WriteLine(str); // 大市民重新赋值
1
2
3
4
5
6
7
8
9
10using System.Text;
StringBuilder str = new StringBuilder("小市民");
// 重新赋值
str.Clear();
str.Append("大老板");
Console.WriteLine(str); // 大老板判断是否相等
1
2
3
4
5
6
7
8
9
10
11
12
13using System.Text;
StringBuilder str = new StringBuilder("小市民");
if(str.Equals("小市民"))
{
Console.WriteLine("相等"); // 打印
}
else
{
Console.WriteLine("不相等");
}
结构体和类的区别
概述:
- 结构体是值类型,类是引用类型,因此所在的内存区域不同
- 结构体不具备继承和多态的特性,因此不能使用
protected
访问修饰符
细节:
- ❌结构体变量在申明时不能赋予初始值,而类可以(这是视频原话,但是翻看笔记和事件发现.net 8是可以的,赋予初值不会报错)
- 结构体无法申明无参构造函数,而类可以
- 结构体申明有参构造后,无参构造不会被顶掉
- 结构题要在构造函数中初始化所有变量的值,而类不需要
- 结构体不能被
static
修饰,而类可以 - 结构体不能在内部申明和自己一样的结构体变量,但是类可以
- 结构体不能被继承,而类可以
- 类可以继承接口(特殊)
如何选择:
- 想要继承和多态特性,选择类
- 对象仅仅是数据集合时,选择结构体
- 从赋值方面考虑,是否原对象需要一起跟着变化
抽象类和接口类的区别
相同点:
- 都可以被继承
- 都不能被实例化
- 都包含方法的申明(不含方法体)
- 子类都必须实现未实现的方法
- 都遵循里氏替换原则
不同点:
- 抽象类可以有构造函数,结构没有
- 抽象类只能被单一继承,而接口可以被继承多个
- 抽象类可以有成员变量,接口不能有成员变量
- 接口中只能申明没有实现的抽象方法
- 抽象类的方法可以使用访问修饰符,接口中建议不写,默认为
public
如何选择:
- 表示对象用抽象类,表示行为用接口
- 不同对象的相同行为,用接口实现