最近在做一些需要用到 C# 开发的项目,许久未碰有些生疏,因此决定把语言的基础内容先都快速过一遍。为方便查阅,挑选最精简的部分整理于该博客。
【说明】
- 本系列的目标是整理出十个以内的博客(传送门),适合快速入门。目前更新到:精简 C# 入门(四)
- 本系列不适用于编程初学者(未接触任何计算机语言),这一类读者建议仔细学习【参考资料】中的视频资源
【参考资料】
1.b站:【01_C#入门到精通】新手强烈推荐:C#开发课程,一整套课程
2.知乎:C#入门到进阶资源汇总
文章目录
#1 函数(方法)
1.1 声明 & 调用
函数的声明
[public] static 返回值类型 函数名([参数列表])
{
// 代码块
}
- 我们将用
static
关键字修饰的函数称之为静态函数,相反的,如果不用则称之为非静态函数,这两者有相当显著的区别,详见 博客园:C#中静态与非静态方法比较 - 返回值类型除了
int
,string
等之外,还可以使用空类型void
。返回值为空类型的函数可以不写return
或者用return
来表示函数的执行结束
以 (C#) 精简 C# 入门(一)Section 4 中的一段代码为例:
class Program
{
public static int GetMax(int n1, int n2)
{
return n1 > n2 ? n1 : n2;
}
}
函数的调用
类名.函数名([参数列表])
示例:
int max = Program.GetMax(1, 2);
其他注意事项
对于一个函数而言,其功能单一并不是缺点,反而是基本要求。
1.2 参数 out
, ref
, params[]
out
输出参数: 使函数能够返回多个类型的值。语法:
[public] static 返回值类型 函数名([参数列表], out 返回值类型 变量名, ...)
示例:
class Program
{
static void Main(string[] args)
{
string str = null;
double n = Program.Test(519, out str);
Console.WriteLine(“{
0}, {
1}", str, n);
Console.ReadKey();
}
static double Test(double n1, out string str1)
{
str1 = "Hello World";
return n1 + 1
}
}
>>>
"Hello World 520"
ref
引用参数:使函数内对变量的更改在函数外仍有效。语法:
[public] static 返回值类型 函数名([参数列表], ref 返回值类型 变量名, ...)
示例:
class Program
{
static void Main(string[] args)
{
int n = 0;
Program.Add_1(n);
Console.WriteLine(n);
Program.Add_2(ref n);
Console.WriteLine(n);
Console.ReadKey();
}
// 不使用 ref
static void Add_1(int n1) {
n1 += 1; }
// 使用 ref
static void Add_2(ref int n1) {
n1 += 1; }
}
>>>
"0
1"
params[]
可变参数数组:将输入参数中与可变参数数组类型一致的元素,均当做数组的元素。可变参数数组只能作为输入参数列表中的最后一个元素,示例:
class Program
{
static void Main(string[] args)
{
Program.studentScore("小红", 98, 100, 99);
Program.studentScore("小白", 65, 32); // 第三次弃考无成绩
Console.ReadKey();
}
static void studentScore(string name, params int[] scores)
{
int sum = 0;
for (int i = 0; i < scores.Length; i++) {
sum += scores[i]; }
Console.WriteLine("{0}'s all score: {1}", name, sum);
}
}
>>>
"小红's all score: 297
小白's all score: 97"
当然,如果直接填 int[]
类型的数组也是 OK 的
1.3 重载 & 递归
重载:
重载指的是多个名称相同,但参数不同的函数(返回值类型不需要相同),使得计算机能够根据输入参数的类型以及个数,来选择相应的需要执行的函数。
示例:以下代码中对 Add
函数的重载使其既可以读取 double
类型的数据,也可以读取 string
来进行相加操作
class Program
{
static void Main(string[] args)
{
double r1 = Add(1, 2);
double r2 = Add("1", "2");
Console.WriteLine("Add double: {0}", r1);
Console.WriteLine("Add string: {0}", r2);
Console.ReadKey();
}
static double Add(double n1, double n2)
{
return n1 + n2;
}
static double Add(string s1, string s2)
{
return Convert.ToDouble(s1) + Convert.ToDouble(s2);
}
}
重载存在限制:
- 如果参数个数相同,那么参数类型就不能相同
- 如果参数类型相同,那么参数个数就不能相同
递归:
自己调用自己。示例:
class Program
{
public static int currentTurn = 0;
static void Main(string[] args)
{
Program.Repeat(3);
Console.ReadKey();
}
static void Repeat(int maxTurn)
{
if (currentTurn < maxTurn)
{
Console.WriteLine("重复一次");
currentTurn++;
Program.Repeat(maxTurn);
}
return;
}
}
>>>
"重复一次
重复一次
重复一次"
1.4 练习
关于函数的上述知识点,【参考资料】(1) 中的视频教程给了非常多很有帮助的练习,传送门:(以下红框内的视频中包含的练习都挺不错的)
#2 飞行棋游戏
仅使用当前所介绍的知识,就足矣写出一款飞行棋游戏,由于内容比较长,建议感兴趣的读者跳转到【参考资料】(1) 中的案例练习,传送门。
作者根据自己的理解写了一个类似功能的游戏,目前还没有做代码优化,因此可以看到将近写了 400 行左右(就效果而言总体满意)。待优化一遍后应该会上传至 github,以下是代码截图以及运行截图:
#3 面向过程 → \to → 面向对象
面向过程:分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现。
面向对象:把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
以上表述摘自 CSDN: 面向对象与面向过程的本质的区别,个人觉得非常直观易懂。
而在【参考资料】(1) 中,传送门,讲师也提供了几个能够帮助理解面向对象和面向过程差异的例子。例如,分别用这两者来描述“关门”:
我们使用 属性
+ 方法
来描述对象。以三个对象,白炽灯、LED灯、小猫为例:
白炽灯
属性:尺寸(20’’)、价格(5 $)
方法:发光(黄色)
LED灯
属性:尺寸(18’’)、价格(9 $)
方法:发光(白色)
小猫
属性:种类(橘猫)、体重(10kg)、性格(闷骚)
方法:吃喝拉撒、与主人玩
上述三个例子当中,很显然白炽灯与LED灯这两个对象具有非常类似的属性和方法,我们把这样的对象进行进一步的封装,从而抽象出 “类” 这一概念。
#4 类
类就是一个模子,确定了某种对象应该具有的属性和方法,因此对象是通过类来创建的,例如白炽灯与LED灯这两个对象可以由 “灯类” 创建 。
类的代码描述:
[public] class 类名
{
Fields 字段;
Properties 属性;
Methods 方法
}
一个关于类的完整示例:
class Pet
{
// 构造函数
public Pet(string species, int age)
{
this.Species = species;
this.Age = age;
}
public Pet(string species) : this(species, 0) {
} // 重载
// 字段
private string _species;
private int _age;
// 属性
public string Species {
get => _species; set => _species = value; }
public int Age
{
get => _age;
set
{
value = value >= 0 ? value : 0;
_age = value;
}
}
// 方法
/// <summary>
/// 实例方法:介绍由该类创建的对象
/// </summary>
public void IntroObj()
{
Console.WriteLine("I'm a {0} years old {1}.", this.Age, this.Species);
}
/// <summary>
/// 静态方法:介绍类
/// </summary>
public static void IntroClass()
{
Console.WriteLine("It's a class about pet, \nincluding some basic properties and functions.");
}
}
以上代码中的新内容将在下列几个 subsection 中依次介绍。
4.1 字段 Fields
字段是属于类的变量,它表示了由类所创建的变量的某种属性。定义方式:
[private] 字段类型 字段名;
关于字段,我们需要知道:
- 关键字
private
则表示不可由外界访问
关键字public
表示可由外界访问(读取、修改)。为了字段的安全性,一般不使用public
- 为了显示字段与方法中的变量的区别,字段名建议以
_
开头 - 使用
this.字段名
的形式在类的内部调用字段,例如上例中的this._age
4.2 属性 Properties
属性用于保护字段,对其读取、修改操作进行限制。属性由两个方法组成:
public 字段类型 属性名 {
get => species; set => species = value; }
get()
表示读取
set()
表示修改
关于属性,我们需要知道:
- 属性一般都定义为
public
- 属性名其对应与字段名应该相同(含义相同),例如属性
Age
对应字段_age
- 选中对应的字段后,使用快捷键
Ctrl+R+E
实现属性的快速生成(更加详细的步骤演示可参考这篇博客:【.NET】VS2017+C#如何快速生成属性) - 为了限制修改,一般情况下会对
set()
方法做一定的修改。
例如在以下代码中,Age
属性就对年龄的修改做了限制,规定必须要大于等于 0 - 只读属性 & 只写属性:顾名思义,只读属性没有
set()
方法,只写属性没有get()
方法
应用示例:
// 类内部
class Pet
{
private int _age;
public int Age {
get => _age;
set {
value = value >= 0 ? value : 0; // 限制对年龄的修改必须大于等于 0
_age = value;
}
}
public void Intro()
{
// 读取。在类内部直接使用 this._age 也完全 OK
Console.WriteLine("I'm {0} years old.", this.Age);
}
}
// 类外部
Pet cat = new Pet();
cat.Age = 2; // 修改
Console.WriteLine(cat.Age); // 读取
>>>
"2"
4.3 方法 Methods
4.3.1 静态 & 非静态
在学习类方法前,首先需要理解 “静态” (static
) 与 “非静态” (也叫 “实例”) 之间的区别:
- 静态成员包含静态字段、静态属性、静态函数,实例成员同理
- 静态类中只能有静态成员,不能有实例成员
非静态类中都能有 - 调用实例成员时,使用
对象名.实例成员名
调用静态成员时,使用类名.静态成员名
- 静态函数只能访问静态成员
实例函数都能访问
在具体使用的时候,如果想要定义一个 “工具类”,可以考虑将其写成静态类。例如 C# 自带的 Console 类
4.3.2 构造函数
作用:初始化对象
public 类名([参数列表])
{
// 赋值
}
示例:
public Pet(string species, int age)
{
this.Species = species;
this.Age = age;
}
构造函数当然也可以重载,在重载构造函数时为了减少代码冗余,可以使用 this
关键字(之前的已经介绍了 this
的两种用法 this.字段名
、this.属性名
):
public 类名([参数列表1]):this([参数列表2]) {
}
构造函数重载的对象一般为最完整的构造函数,因此需要在 参数列表1
加入一些默认初值来组成 参数列表2
,例如下例中第二个构造函数将 age 默认为 0
public Pet(string species, int age)
{
this.Species = species;
this.Age = age;
}
public Pet(string species):this(species, 0) {
}
4.3.3 析构函数
作用:在程序结束时会自动调用,帮助释放占用的内存资源
~类名() {
}
示例:
~Pet() {
}