C#(读作"C Sharp")是一种现代的、通用的面向对象编程语言,由微软公司开发。它结合了C和C++的强大特性,并去掉了一些复杂性,使得开发者可以更加高效地编写代码。
一、入坑C#
(一) 安装和设置
首先,确保你的计算机上安装了 .NET SDK。这个SDK包含了编译和运行C#程序所需的工具。
提供一种在线运行哦,如果你不是为了写项目,只是为了来学语法的,你可以使用在线运行的方式,就不用安装环境啦。
(二) 第一个C#程序
打开你喜欢的文本编辑器(如Visual Studio Code、Visual Studio等),创建一个新文件,命名为 HelloWorld.cs
。
using System; // 引入System命名空间,包含了Console类等核心功能class Program
{static void Main() // 主方法,程序的入口点{Console.WriteLine("Hello, World!"); // 在控制台输出Hello, World!}
}
(三) 编译和运行
打开命令行工具(如PowerShell或终端),进入到保存 HelloWorld.cs
文件的目录。运行以下命令编译和运行程序:
dotnet run HelloWorld.cs
你应该会看到输出:
Hello, World!
恭喜!你已经成功运行了你的第一个C#程序。
(四) 在线运行
进入传送门
二、基本语法和结构
(一) 变量和数据类型
在 C# 中,变量分为以下几种类型:
- 值类型(Value types)
- 引用类型(Reference types)
- 指针类型(Pointer types)
1. 值类型
值类型变量可以直接分配给一个值。它们是从类 System.ValueType
中派生的。
值类型直接包含数据。比如 int
、char
、float
,它们分别存储 数字、字符、浮点数。
类型 | 描述 | 范围 | 默认值 |
bool | 布尔值 | True 或 False | False |
byte | 8 位无符号整数 | 0 到 255 | 0 |
char | 16 位 Unicode 字符 | U +0000 到 U +ffff | '\0' |
decimal | 128 位精确的十进制值,28-29 有效位数 | (-7.9 x 1028 到 7.9 x 1028) / 100 到 28 | 0.0M |
double | 64 位双精度浮点型 | (+/-)5.0 x 10-324 到 (+/-)1.7 x 10308 | 0.0D |
float | 32 位单精度浮点型 | -3.4 x 1038 到 + 3.4 x 1038 | 0.0F |
int | 32 位有符号整数类型 | -2,147,483,648 到 2,147,483,647 | 0 |
long | 64 位有符号整数类型 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 | 0L |
sbyte | 8 位有符号整数类型 | -128 到 127 | 0 |
short | 16 位有符号整数类型 | -32,768 到 32,767 | 0 |
uint | 32 位无符号整数类型 | 0 到 4,294,967,295 | 0 |
ulong | 64 位无符号整数类型 | 0 到 18,446,744,073,709,551,615 | 0 |
ushort | 16 位无符号整数类型 | 0 到 65,535 | 0 |
在C#中,变量需要显式声明其类型。例如:
int age = 25;
string name = "悟解";
double salary = 2500.50;
bool isEmployed = true;
2. 引用类型
当使用引用类型时,变量不直接包含实际数据,而是包含对数据所在内存位置的引用。换句话说,它们指向内存中的一个位置。多个变量可以指向同一个内存位置。如果一个变量修改了这个位置的数据,其他变量也会自动反映出这些值的变化。内置的引用类型包括:object、string。
一、对象类型
对象类型(Object Type)是C#通用类型系统中所有数据类型的最终基类。Object
类型是System.Object
类的别名。因此,对象类型可以存储任何其他类型(值类型、引用类型、预定义类型或用户定义类型)的值。但在分配值之前,需要进行类型转换。
当将值类型转换为对象类型时,这称为装箱(boxing);反之,当对象类型转换为值类型时,则称为拆箱(unboxing)。
object obj;
object obj = 100; // 装箱
int num = (int)obj; // 拆箱
Console.WriteLine(num); // 输出:100
二、动态类型(Dynamic Type)
动态类型变量可以存储任何类型的值,并且其类型检查是在运行时而非编译时进行的。
声明动态类型变量的语法如下:
dynamic <variable_name> = value;
例如:
dynamic d = 20;
动态类型与对象类型相似,但是对象类型变量的类型检查是在编译时进行的,而动态类型变量的类型检查是在运行时进行的。
三、字符串(String)类型
字符串(String)类型允许你存储任意字符串值。它是System.String
类的别名,从Object类型派生而来。字符串类型的值可以通过两种方式分配:使用引号和@引号。
// 使用双引号分配字符串值给变量
string str1 = "悟解";// 使用@符号来创建逐字字符串,转义字符会被当作普通字符处理
string str2 = @"C:\悟解";// @字符串可以包含任意换行符和缩进空格
string str3 = @"<script type=""text/javascript""><!--悟解来学C#啦!!!--></script>";// 字符串的长度包括换行符和缩进空格
int str3Length = str3.Length;
四、指针类型(Pointer types)
允许存储另一种类型的内存地址。在C#中,指针与C或C++中的指针功能类似。
char* c;
int* i;
这些声明定义了分别指向字符(char)和整数(int)类型的指针变量。
3. C# 类型转换详解
在 C# 编程中,类型转换是将一个数据类型的值转换为另一个数据类型的过程。C# 提供了两种主要的类型转换方式:隐式类型转换和显式类型转换(也称为强制类型转换)。
一、隐式类型转换
隐式类型转换是由编译器自动完成的转换过程,不需要显示指定类型转换操作。这种转换通常发生在从较小范围数据类型向较大范围数据类型(小->大)的转换中,它是安全的不会导致数据丢失。
例如,将一个整数赋值给一个长整数或者将一个浮点数赋值给一个双精度浮点数都属于隐式类型转换。
byte a = 10;
int i = a; // 隐式转换,不需要显式转换,编译器会自动完成类型转换
二、显式类型转换
显式类型转换(或称为强制类型转换)需要程序员在代码中明确指定转换操作,它发生在从较大范围数据类型向较小范围数据类型(大->小)的转换中,或者在对象类型之间的转换中。显式类型转换可能会导致数据丢失,因此需要谨慎使用。
double d = 3.14;
int i = (int)d; // 强制从 double 到 int,数据可能损失小数部分
另一个常见的显式类型转换是将整数转换为字符串:
int i = 123;
string s = i.ToString(); // 将 int 转换为字符串
上述代码中,i.ToString()
方法将 i
的整数值转换为其字符串表示形式。
三、类型转换
使用 Convert
类、Parse
方法和 TryParse
方法进行类型转换是在 C# 中处理数据类型转换常见且有效的方式。
(一) 使用 Convert 类
Convert
类提供了方便的静态方法,用于在各种基本数据类型之间进行转换。
这些方法都定义在 System.Convert
类中,使用时需要包含 System 命名空间。它们提供了一种安全的方式来执行类型转换,因为它们可以处理 null值,并且会抛出异常,如果转换不可能进行。
方法 | 描述 |
ToBoolean | 如果可能的话,把类型转换为布尔型。 |
ToByte | 把类型转换为字节类型。 |
ToChar | 如果可能的话,把类型转换为单个 Unicode 字符类型。 |
ToDateTime | 把类型(整数或字符串类型)转换为 日期-时间 结构。 |
ToDecimal | 把浮点型或整数类型转换为十进制类型。 |
ToDouble | 把类型转换为双精度浮点型。 |
ToInt16 | 把类型转换为 16 位整数类型。 |
ToInt32 | 把类型转换为 32 位整数类型。 |
ToInt64 | 把类型转换为 64 位整数类型。 |
ToSbyte | 把类型转换为有符号字节类型。 |
ToSingle | 把类型转换为小浮点数类型。 |
ToString | 把类型转换为字符串类型。 |
ToType | 把类型转换为指定类型。 |
ToUInt16 | 把类型转换为 16 位无符号整数类型。 |
ToUInt32 | 把类型转换为 32 位无符号整数类型。 |
ToUInt64 | 把类型转换为 64 位无符号整数类型。 |
string str = "123";
int num = Convert.ToInt32(str);
在上面的示例中,将字符串 "123"
转换为整数类型 int
,并赋值给变量 num
。
(二) 使用 Parse 方法
Parse
方法用于将字符串转换为对应的数值类型。如果字符串格式不正确,将会抛出异常。
方法 | 描述 |
| 将字符串解析为 |
| 将字符串解析为 |
| 将字符串解析为 |
| 将字符串解析为 |
| 将字符串解析为 |
| 将字符串解析为 |
| 将字符串解析为 |
| 将字符串解析为 |
| 将字符串解析为 |
| 将字符串解析为 |
| 将字符串解析为 |
| 将字符串解析为 |
| 将字符串解析为 |
| 将字符串解析为 |
string str = "6.66";
double d = double.Parse(str);
在上面的示例中,将字符串 "6.66"
转换为双精度浮点数类型 double
,并赋值给变量 d
。
(三) 使用 TryParse 方法
TryParse
方法类似于 Parse
,但不会抛出异常。它返回一个布尔值,指示转换是否成功,并通过 out
参数返回转换后的值。
方法 | 描述 |
| 尝试将字符串解析为 |
| 尝试将字符串解析为 |
| 尝试将字符串解析为 |
| 尝试将字符串解析为 |
| 尝试将字符串解析为 |
| 尝试将字符串解析为 |
| 尝试将字符串解析为 |
| 尝试将字符串解析为 |
| 尝试将字符串解析为 |
| 尝试将字符串解析为 |
| 尝试将字符串解析为 |
| 尝试将字符串解析为 |
| 尝试将字符串解析为 |
| 尝试将字符串解析为 |
string str = "123.45";
double d;
bool success = double.TryParse(str, out d);if (success) {Console.WriteLine("转换成功: " + d);
} else {Console.WriteLine("转换失败");
}
(四) 自定义类型转换
在C#中,你可以通过定义特定的运算符重载方法来实现自定义类型转换。这些方法允许你在不同类型之间进行显式或隐式转换。在C#中,有两种类型转换的关键字:
- implicit (隐式):隐式转换方法允许编译器在不需要显式转换的情况下自动进行类型转换。通常用于从小范围类型到大范围类型的转换,或者在没有数据丢失的情况下进行转换。使用
implicit
关键字定义的转换方法,必须满足安全转换的条件,即不能造成数据丢失或精度降低。 - explicit (显式):显式转换方法要求在代码中明确地使用强制类型转换运算符来指定转换的方式。这种方式适用于可能会造成数据丢失的转换,或者在语义上不太明确的转换操作。使用
explicit
关键字定义的转换方法,需要通过强制类型转换来调用。
让我们通过一个简单的例子来说明如何在自定义类型中定义这两种转换方法。
using System;// 自定义温度类
public class Temperature {public double Celsius { get; }// 构造函数public Temperature(double celsius) {Celsius = celsius;}// 隐式转换:从摄氏度到华氏度public static implicit operator Fahrenheit(Temperature t) {double fahrenheit = t.Celsius * 9 / 5 + 32;return new Fahrenheit(fahrenheit);}// 显式转换:从摄氏度到开尔文public static explicit operator Kelvin(Temperature t) {double kelvin = t.Celsius + 273.15;return new Kelvin(kelvin);}
}// 华氏度类
public class Fahrenheit {public double Value { get; }public Fahrenheit(double value) {Value = value;}
}// 开尔文类
public class Kelvin {public double Value { get; }public Kelvin(double value) {Value = value;}
}class Program {static void Main() {Temperature tempCelsius = new Temperature(25.0);// 隐式转换:摄氏度到华氏度Fahrenheit tempFahrenheit = tempCelsius;Console.WriteLine($"华氏度: {tempFahrenheit.Value}");// 显式转换:摄氏度到开尔文Kelvin tempKelvin = (Kelvin)tempCelsius;Console.WriteLine($"开尔文: {tempKelvin.Value}");}
}
(五) 可空类型
在C#中,使用单问号 ?
和双问号 ??
来处理可空类型是很常见的。单问号 ?
用于将不能直接赋值为 null
的数据类型(如 int
、double
、bool
)转换为可赋值为 null
的可空类型。这意味着该数据类型现在是可空的。
int? i = 3;
// 等同于:
Nullable<int> i = new Nullable<int>(3);
另一方面,双问号 ??
用于判断一个变量是否为 null
,如果是,则返回一个指定的默认值。
(二) 运算符
1. 算术运算符
下表显示了 C# 支持的所有算术运算符。假设变量 A = 10,变量 B = 20,则:
运算符 | 描述 | 实例 |
+ | 把两个操作数相加 | A + B 将得到 30 |
- | 从第一个操作数中减去第二个操作数 | A - B 将得到 -10 |
* | 把两个操作数相乘 | A * B 将得到 200 |
/ | 分子除以分母 | B / A 将得到 2 |
% | 取模运算符,整除后的余数 | B % A 将得到 0 |
++ | 自增运算符,整数值增加 1 | A++ 将得到 11 |
-- | 自减运算符,整数值减少 1 | A-- 将得到 9 |
using System;namespace OperatorsDemo
{class Program{static void Main(string[] args){int A = 10;int B = 20;// 加法运算符 +int addition = A + B;Console.WriteLine($"A + B = {addition}"); // 输出:A + B = 30Console.ReadLine();// 减法运算符 -int subtraction = A - B;Console.WriteLine($"A - B = {subtraction}"); // 输出:A - B = -10Console.ReadLine();// 乘法运算符 *int multiplication = A * B;Console.WriteLine($"A * B = {multiplication}"); // 输出:A * B = 200Console.ReadLine();// 除法运算符 /int division = B / A;Console.WriteLine($"B / A = {division}"); // 输出:B / A = 2Console.ReadLine();// 取模运算符 %int modulus = B % A;Console.WriteLine($"B % A = {modulus}"); // 输出:B % A = 0Console.ReadLine();// 自增运算符 ++A++; // A = 11Console.WriteLine($"A++ = {A}"); // 输出:A++ = 11Console.ReadLine();// 自减运算符 --A--; // A = 10Console.WriteLine($"A-- = {A}"); // 输出:A-- = 10Console.ReadLine();}}
}
2. 关系运算符
下表显示了 C# 支持的所有关系运算符。假设变量 A 的值为 10,变量 B 的值为 20,则:
运算符 | 描述 | 实例 |
== | 检查两个操作数的值是否相等,如果相等则条件为真。 | (A == B) 不为真。 |
!= | 检查两个操作数的值是否相等,如果不相等则条件为真。 | (A != B) 为真。 |
> | 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 | (A > B) 不为真。 |
< | 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 | (A < B) 为真。 |
>= | 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 | (A >= B) 不为真。 |
<= | 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 | (A <= B) 为真。 |
3. 逻辑运算符
下表显示了 C# 支持的所有逻辑运算符。假设变量 A 为布尔值 true,变量 B 为布尔值 false,则:
运算符 | 描述 | 实例 |
&& | 称为逻辑与运算符。如果两个操作数都非零,则条件为真。 | (A && B) 为假。 |
|| | 称为逻辑或运算符。如果两个操作数中有任意一个非零,则条件为真。 | (A || B) 为真。 |
! | 称为逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。 | !(A && B) 为真。 |
4. 位运算符
位运算符作用于位,并逐位执行操作。以下是常用的位运算符及其真值表:
p | q | p & q | p | q | p ^ q |
0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 1 | 1 |
1 | 1 | 1 | 1 | 0 |
1 | 0 | 0 | 1 | 1 |
假设如果 A = 60,且 B = 13,现在以二进制格式表示,它们如下所示:
A = 0011 1100
B = 0000 1101执行以下位运算:A & B = 0000 1100,结果为 12A | B = 0011 1101,结果为 61A ^ B = 0011 0001,结果为 49~A = 1100 0011,结果为 -61(以补码形式表示)
下表列出了 C# 支持的位运算符。假设变量 A 的值为 60,变量 B 的值为 13,则:
运算符 | 描述 | 示例 |
& | 如果同时存在于两个操作数中,二进制 AND 运算符复制一位到结果中。 | (A & B) 将得到 12,即为 0000 1100 |
| | 如果存在于任一操作数中,二进制 OR 运算符复制一位到结果中。 | (A | B) 将得到 61,即为 0011 1101 |
^ | 如果存在于其中一个操作数中但不同时存在于两个操作数中,二进制异或运算符复制一位到结果中。 | (A ^ B) 将得到 49,即为 0011 0001 |
~ | 按位取反运算符是一元运算符,具有"翻转"位效果。包括符号位。 | (~A ) 将得到 -61,即为 1100 0011 |
<< | 二进制左移运算符。左操作数的值向左移动右操作数指定的位数。 | A << 2 将得到 240,即为 1111 0000 |
>> | 二进制右移运算符。左操作数的值向右移动右操作数指定的位数。 | A >> 2 将得到 15,即为 0000 1111 |
这些运算符在处理位级别的操作时非常有用,能够直接操作数据的二进制表示。
补充知识:
- 左移运算符 (<<)
左移运算符将一个数的二进制表示向左移动指定的位数。
在左移运算中,右侧的空位用0填充。
左移运算相当于将原数乘以 2 的 n 次方,其中 n 是左移的位数。
对于任何整数 A 和非负整数 n,A << n 等价于
A * (2^n)
。
示例:
如果 A = 5 (二进制表示为 0000 0101),
A << 2 将 A 向左移动两位:0000 0101 变成 0001 0100,结果为 20,即为5*(2^2)。
- 右移运算符 (>>)
右移运算符将一个数的二进制表示向右移动指定的位数。
右移运算符可以是算术右移或逻辑右移。
右移运算相当于将原数除以 2 的 n 次方,其中 n 是右移的位数。
算术右移适用于有符号整数,而逻辑右移适用于无符号整数。
- 算术右移:
对于带符号的整数,算术右移会将最高位(符号位)复制并向右移动。
即如果原数是正数,则用0填充最高位;
如果原数是负数,则用1填充最高位。
示例:
如果 A = -8 (二进制表示为 1111 1000),
A >> 2 将 A 右移两位:1111 1000 变成 1111 1110,结果为 -2。
- 逻辑右移:
对于无符号数,逻辑右移会在左侧插入0,右侧的空位用0填充。
示例:
如果 A = 8 (二进制表示为 0000 1000),
A >> 2 将 A 右移两位:0000 1000 变成 0000 0010,结果为 2。
5. 赋值运算符
下表列出了 C# 支持的赋值运算符:
运算符 | 描述 | 实例 |
= | 简单的赋值运算符,把右边操作数的值赋给左边操作数 | C = A + B 将把 A + B 的值赋给 C |
+= | 加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数 | C += A 相当于 C = C + A |
-= | 减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数 | C -= A 相当于 C = C - A |
*= | 乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数 | C *= A 相当于 C = C * A |
/= | 除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数 | C /= A 相当于 C = C / A |
%= | 求模且赋值运算符,求两个操作数的模赋值给左边操作数 | C %= A 相当于 C = C % A |
<<= | 左移且赋值运算符 | C <<= 2 等同于 C = C << 2 |
>>= | 右移且赋值运算符 | C >>= 2 等同于 C = C >> 2 |
&= | 按位与且赋值运算符 | C &= 2 等同于 C = C & 2 |
^= | 按位异或且赋值运算符 | C ^= 2 等同于 C = C ^ 2 |
|= | 按位或且赋值运算符 | C |= 2 等同于 C = C | 2 |
public static void Main(string[] args)
{// sizeof 运算符示例int size = sizeof(int); // 获取 int 类型的大小Console.WriteLine($"int 类型的大小为 {size} 字节"); // int 类型的大小为 4 字节// typeof 运算符示例Type type = typeof(int); // 获取 int 类型的 Type 对象Console.WriteLine($"Int 类型的完整名称为 {type.FullName}"); // Int 类型的完整名称为 System.Int32// ?: 运算符示例int x = 5;string result = (x > 0) ? "x 大于 0" : "x 不大于 0";Console.WriteLine(result); // x 大于 0// is 运算符示例object obj = "Hello";if (obj is string){ Console.WriteLine("obj 是 string 类型"); } // obj 是 string 类型// as 运算符示例object anotherObj = "World";string str = anotherObj as string;if (str != null){Console.WriteLine($"转换成功:{str}"); // 转换成功:World}
}
6. 可空类型(?)
C#提供了一种特殊的数据类型,叫做可空类型(nullable types),它可以表示基础值类型的正常范围内的值,还可以表示 null
值。
例如,Nullable<Int32>
(或简写为 int?
)表示一个整数可以赋值为 -2,147,483,648
到 2,147,483,647
之间的任何值,或者可以赋值为 null
。类似地,Nullable<bool>
变量可以赋值为 true
、false
或 null
。
在处理数据库或其他可能未赋值的数据类型时,将 null
赋给数值类型或布尔类型是非常有用的。例如,数据库中的布尔字段可以存储 true
或 false
,或者该字段可以是未定义的。
声明一个可空类型的语法如下:
<数据类型>? <变量名> = null;
以下是可空数据类型用法的示例:
using System;namespace CalculatorApplication
{class NullableTypesExample{static void Main(string[] args){int? num1 = null;int? num2 = 45;double? num3 = new double?();double? num4 = 3.14157;bool? boolVal = new bool?();// 输出值Console.WriteLine("显示可空类型的值: {0}, {1}, {2}, {3}", num1, num2, num3, num4);Console.WriteLine("一个可空的布尔值: {0}", boolVal);}}
}
当上述代码被编译和执行时,会输出以下结果:
显示可空类型的值: , 45, , 3.14157
一个可空的布尔值:
7. Null 合并运算符( ?? )
Null 合并运算符用于定义可空类型或引用类型的默认值。它为可能为 null
的值类型定义一个预设值。
如果第一个操作数的值为 null
,则该运算符返回第二个操作数的值;否则返回第一个操作数的值。以下是一个示例:
using System;namespace CalculatorApplication
{class NullableTypesExample{static void Main(string[] args){double? num1 = null;double? num2 = 3.14157;double num3;num3 = num1 ?? 5.34; // 如果 num1 为 null,则返回 5.34Console.WriteLine("num3 的值: {0}", num3); // 5.34// ?? 可以理解为三元运算符的简化形式:num3 = (num1 == null) ? 5.34 : num1;Console.WriteLine("num3 的值: {0}", num3); // 5.34num3 = num2 ?? 5.34;Console.WriteLine("num3 的值: {0}", num3); // 3.14157}}
}
当上述代码被编译和执行时,会输出以下结果:
num3 的值: 5.34
num3 的值: 5.34
num3 的值: 3.14157
(三) 控制流程
C#支持常见的控制流结构,如 if
、else
、for
、while
等:
? :
运算符可以用来替代if...else
语句。
它的一般形式如下:
Exp1 ? Exp2 : Exp3;
其中,Exp1、Exp2 和 Exp3 是表达式。请注意,冒号的使用和位置。
? 表达式的值是由 Exp1 决定的。
如果 Exp1 为真,则计算 Exp2 的值,结果即为整个 ? 表达式的值。
如果 Exp1 为假,则计算 Exp3 的值,结果即为整个 ? 表达式的值。
using System;class Program
{static void Main(){for (int i = 1; i <= 10; i++){string result = (i % 2 == 0) ? "偶数" : "奇数";Console.WriteLine($"{i}是{result}。");//到5就停止if (i == 5) break;}}
}
(四) 函数和方法
在C#中,函数(Function)和方法(Method)是可重用的代码块,用于执行特定的任务或操作。它们封装了一系列语句,可以接受输入参数并返回一个值。这两个术语通常可以互换使用,但在面向对象的语境下,我们更倾向于使用“方法”来描述类中的函数。
1. 函数与方法的定义
方法是类中的函数,它们被用来描述类的行为。方法定义的结构如下:
访问修饰符 返回类型 方法名(参数列表)
{// 方法体,包含一系列语句// 可选,使用 return 语句返回值
}
2. 函数与方法的使用方式
一、调用函数与方法
调用函数或方法是通过其名称和适当的参数列表来实现的。例如:
static int Add(int a, int b)
{return a + b;
}// 调用函数
int sum = Add(3, 5);
二、参数与返回值
- 参数:函数和方法可以接受零个或多个参数,这些参数可以是任意的数据类型。例如,上面的
Add
函数接受两个int
类型的参数a
和b
。 - 返回值:函数和方法可以返回一个值,该值与函数或方法的返回类型相对应。例如,
Add
函数返回一个int
类型的结果。
当我们谈论编程中的递归方法调用时,我们指的是一个方法可以直接或间接地调用自身。这种编程技术允许我们在解决问题时通过重复应用相同的算法来简化代码,尤其是在处理具有递归结构的问题时特别有用。在本文中,我们将探讨递归的基本概念,并通过一个经典的例子——计算阶乘来解释递归方法的实现及其应用。
三、递归调用
递归在计算机科学中是指一个函数通过直接或间接调用自身来解决问题的方法。它是一种将问题分解为更小、更易解决的子问题的技术。递归方法通常包括两个关键部分:
- 基本情况(Base Case):这是递归算法中的结束条件。在基本情况下,递归函数不再调用自身,而是返回一个明确的值或执行某个特定的操作。
- 递归情况(Recursive Case):在递归算法中,递归情况是指问题的规模不断减小,通过调用自身来解决更小规模的子问题。
让我们通过一个简单而经典的例子来说明递归方法的实现:计算一个正整数的阶乘。阶乘的定义如下:
- ( n! = n(n-1) (n-2) ··· 1 )
- 特别地,( 0! = 1 )
using System;class NumberManipulator
{public int factorial(int num){return (num == 1) ? 1 : factorial(num - 1) * num;}static void Main(string[] args){NumberManipulator n = new NumberManipulator();// 调用 factorial 方法来计算不同数的阶乘Console.WriteLine("5 的阶乘是: {0}", n.factorial(5));}
}
递归方法的优点在于它可以简化某些问题的解决方案,并使代码更加简洁和易于理解。然而,递归方法也可能会导致性能下降(尤其是在没有正确优化的情况下),并且可能会在处理大规模问题时导致堆栈溢出(Stack Overflow)的问题。
(五) 参数传递
当调用带有参数的方法时,您需要向方法传递参数。在 C# 中有三种向方法传递参数的方式:
方式 | 描述 |
值参数 | 这种方式复制参数的实际值给函数的形式参数,实参和形参使用的是两个不同内存中的值。在这种情况下,当形参的值发生改变时,不会影响实参的值,从而保证了实参数据的安全。 |
引用参数 | 这种方式复制参数的内存位置的引用给形式参数。这意味着,当形参的值发生改变时,同时也改变实参的值。 |
输出参数 | 这种方式可以返回多个值。 |
1. 值参数(Value Parameters)
值参数是最常见的参数传递方式之一。当向一个方法传递值参数时,实际上是将实参的值复制到方法中的形参。这意味着在方法内部对形参的任何更改都不会影响到原始的实参。这种方式确保了实参数据的安全性,因为方法无法直接修改实参的值。
using System;class Program
{// 定义一个方法,接受值参数(静态方法)static void ModifyValue(int x){x = x * 2; // 修改形参 x 的值Console.WriteLine("形参 x 的值: " + x);}static void Main(){int number = 10; // 定义一个实参 numberConsole.WriteLine("实参 number 的初始值: " + number); // 10ModifyValue(number); // 调用 ModifyValue 方法,并传递 number 作为值参数Console.WriteLine("方法调用后,实参 number 的值不变: " + number); //10}
}
在上述代码中,ModifyValue
方法接受一个整数值参数 x
,并将其值乘以2。尽管在方法内部修改了 x
的值,但在 Main
方法中,number
的值保持不变。
这展示了值参数的特性:方法内部对形参的更改不会影响到原始的实参。
2. 引用参数(Reference Parameters)
引用参数允许方法直接访问实参的内存位置,从而可以直接修改实参的值。在C#中,使用 ref
关键字声明引用参数。引用参数使得可以在方法内部对实参进行修改,这在需要在方法内改变变量值而不通过返回值时非常有用。
以下是一个使用引用参数的示例:
using System;class Program
{// 定义一个方法,接受引用参数static void ModifyReference(ref int y){y = y * 2; // 修改引用参数 y 的值Console.WriteLine("引用参数 y 的值: " + y);}static void Main(){int number = 10; // 定义一个实参 numberConsole.WriteLine("实参 number 的初始值: " + number); // 10ModifyReference(ref number); // 使用 ref 关键字调用 ModifyReference 方法,并传递 number 作为引用参数Console.WriteLine("方法调用后,实参 number 的值已更改: " + number); // 20}
}
在这个例子中,ModifyReference
方法接受一个整数引用参数 y
,并将其值乘以2。通过 ref
关键字,方法可以直接修改 number
的值,因此在 Main
方法中输出时,number
的值已经改变了。
3. 输出参数(Output Parameters)
输出参数允许方法返回多个值。在C#中,使用 out
关键字声明输出参数。与引用参数不同的是,输出参数在进入方法之前不需要初始化,而引用参数则需要。
以下是一个使用输出参数的示例:
using System;class Program
{// 定义一个方法,接受输出参数static void GetMultipleValues(out int a, out int b){a = 5;b = 10;}static void Main(){int x, y; // 定义实参 x 和 yGetMultipleValues(out x, out y); // 使用 out 关键字调用 GetMultipleValues 方法,并传递 x 和 y 作为输出参数Console.WriteLine("输出参数 x 的值: " + x); // 5Console.WriteLine("输出参数 y 的值: " + y); // 10}
}
在这个例子中,GetMultipleValues
方法使用 out
关键字声明两个输出参数 a
和 b
,并分别将它们初始化为 5 和 10。通过使用 out
关键字,方法可以返回多个值,这些值可以在方法调用后直接被访问和使用。
(六) 数组(Array)
数组是一个存储相同类型元素的固定大小的顺序集合。数组是用来存储数据的集合,通常认为数组是一个同一类型变量的集合。
1. 声明数组
在C#中声明数组可以通过几种不同的方式来实现,取决于数组的类型和初始化需求。以下是几种常见的方式:
一、声明并初始化数组
使用以下语法可以声明并初始化一个数组:
// 声明一个整数数组并初始化
int[] numbers = { 1, 2, 3, 4, 5 };// 声明一个字符串数组并初始化
string[] names = { "Alice", "Bob", "Charlie" };// 声明一个对象数组并初始化
object[] objects = { 1, "two", 3.0, 4.0 };
在这种方式下,数组会根据初始化的内容自动确定长度。
二、声明数组并指定长度
如果需要指定数组的长度,可以使用以下方式声明:
// 声明一个长度为5的整数数组
int[] numbers = new int[5];// 声明一个长度为3的字符串数组
string[] names = new string[3];
这种方式创建了一个指定长度的数组,每个元素的默认值根据类型而定(例如,整数数组的默认值为0,字符串数组的默认值为null)。
三、声明多维数组
C#支持多维数组,可以通过以下方式声明:
// 声明一个二维整数数组
int[,] matrix = new int[3, 3];// 声明一个三维字符串数组
string[,,] cube = new string[3, 3, 3];
多维数组需要指定每个维度的大小。
四、使用数组初始化器
数组初始化器允许在声明时直接初始化数组的元素,类似于第一种方式的简写形式:
// 声明并初始化一个整数数组
int[] numbers = new int[] { 1, 2, 3, 4, 5 };// 声明并初始化一个字符串数组
string[] names = new string[] { "悟解", "误解", "无解" };// 声明并初始化一个对象数组
object[] objects = new object[] { 1, "two", 3.0, 4.0 };
五、隐式类型数组
从C# 3.0开始,可以使用 var
关键字声明数组,编译器会根据初始化时提供的值自动推断数组的类型:
// 隐式声明一个整数数组
var numbers = new[] { 1, 2, 3, 4, 5 };// 隐式声明一个字符串数组
var names = new[] { "Alice", "Bob", "Charlie" };// 隐式声明一个对象数组
var objects = new[] { 1, "two", 3.0, 4.0 };
2. 访问数组
一、使用索引访问数组元素
数组的索引从0开始,通过方括号 []
中的索引来访问数组元素。例如:
int[] numbers = { 10, 20, 30, 40, 50 };// 访问数组中的第一个元素
int firstElement = numbers[0]; // 结果为 10// 访问数组中的第三个元素
int thirdElement = numbers[2]; // 结果为 30
二、修改数组元素
你也可以通过索引来修改数组中的元素:
int[] numbers = { 10, 20, 30, 40, 50 };// 修改数组中的第二个元素
numbers[1] = 25;// 现在 numbers 数组变为 { 10, 25, 30, 40, 50 }
三、多维数组的访问
对于多维数组,需要提供多个索引来访问特定位置的元素:
int[,] matrix = new int[3, 3]
{{ 1, 2, 3 },{ 4, 5, 6 },{ 7, 8, 9 }
};// 访问第二行第三列的元素
int element = matrix[1, 2]; // 结果为 6
四、使用循环遍历数组
通常情况下使用循环来遍历数组中的所有元素。例如,使用 for
循环遍历一维数组:
int[] numbers = { 10, 20, 30, 40, 50 };for (int i = 0; i < numbers.Length; i++)
{ Console.WriteLine(numbers[i]); }
或者使用 foreach
循环遍历一维数组或多维数组:
int[] numbers = { 10, 20, 30, 40, 50 };foreach (int num in numbers)
{ Console.WriteLine(num); }
int[,] matrix = new int[3, 3]
{{ 1, 2, 3 },{ 4, 5, 6 },{ 7, 8, 9 }
};foreach (int num in matrix)
{Console.WriteLine(num);
}
(七) 字符串(string)
在 C# 中,可以使用字符数组来表示字符串,但是,更常见的做法是使用 string
关键字来声明一个字符串变量。string 关键字是 System.String
类的别名。
1. 创建 String 对象
在 C# 中,字符串可以通过多种方式来创建和操作。下面我会详细解释每种方法:
一、string 关键字声明字符串变量
string str1 = "Hello, World!";
这是最常见的创建字符串的方式。在 C# 中,字符串被视为不可变的,这意味着一旦创建了字符串对象,它的值就不能被更改。
二、 String 类的构造函数
string str2 = new String('c', 5); // 创建一个由字符 'c' 组成的长度为 5 的字符串:"ccccc"
三、字符串串联运算符(+)
string firstName = "John";
string lastName = "Doe";
string fullName = firstName + " " + lastName; // 字符串串联操作:"John Doe"
四、ToString方法
有些属性或方法返回字符串类型,可以直接使用它们来创建字符串:
// 使用 DateTime 类的 ToString 方法获取当前时间的字符串表示形式
string timestamp = DateTime.Now.ToString();
五、Format方法
int number = 42;
// 使用 string.Format 格式化字符串:"The answer is 42."
string str3 = string.Format("The answer is {0}.", number);
六、$符号创建
string name = "Alice";
string greeting = $"Hello, {name}!";
2. 常用属性
一、Chars 属性
Chars
属性允许你通过索引访问字符串中的每一个字符,返回的是 char
类型的字符。
- 属性类型:
char
- 索引范围: 从 0 开始到字符串长度减一。
示例使用方法:
string str = "Hello";char firstChar = str.Chars[0]; // 获取第一个字符 'H'
char lastChar = str.Chars[str.Length - 1]; // 获取最后一个字符 'o'Console.WriteLine(firstChar); // 输出: H
Console.WriteLine(lastChar); // 输出:o
二、Length 属性
Length
属性返回当前字符串中的字符数(即字符串的长度)。
- 属性类型:
int
- 返回值: 字符串中字符的总数。
示例使用方法:
string str = "Hello";
int length = str.Length; // 获取字符串的长度,这里是 5
3. 常用操作
一、比较字符串
在 C# 中,比较字符串通常使用 ==
操作符、string.Equals()
或 String.Compare()
方法。
- 使用
==
操作符:用于比较两个字符串的内容是否相等。
string str1 = "hello";
string str2 = "HELLO";string res = (str1 == str2) ? "相同" : "不相同";
- 使用
string.Equals()
方法:除了比较内容是否相等外,还可以指定比较的规则(如忽略大小写)。
string str1 = "hello";
string str2 = "HELLO";string result = str1.Equals(str2) ? "相等" : "不相等";// 使用三目运算符判断两个字符串是否相等(忽略大小写),并输出相应的中文结果
result = str1.Equals(str2, StringComparison.OrdinalIgnoreCase) ? "相等" : "不相等";
- 使用
string.Compare()
方法:静态方法。
string str1 = "hello";
string str2 = "HELLO";string res = (String.Compare(str1, str2) == 0) ? "相同" : "不相同";
二、字符串包含字符串
要检查一个字符串是否包含另一个字符串,可以使用 string.Contains()
方法。
string str = "Hello, world";string result = str.Contains("world") ? "包含" : "不包含";
三、获取子字符串
使用 string.Substring()
方法来获取原始字符串的一部分,即子字符串。
string str = "Hello, world";// 获取从索引为 7 开始的子字符串,包括索引 7 的字符
string subStr1 = str.Substring(7); // world// 获取从索引为 7 开始,长度为 5 的子字符串
string subStr2 = str.Substring(7, 5); //world
四、连接字符串
连接字符串可以使用 +
操作符或 string.Concat()
方法。
- 使用
+
操作符:
string str1 = "Hello";
string str2 = "world";string combined1 = str1 + ", " + str2; // Hello, world
- 使用
string.Concat()
方法:
string str1 = "Hello";
string str2 = "world";string combined2 = string.Concat(str1, ", ", str2); // Hello, world
4. 字符串和时间
一、string.Format
DateTime dt = new DateTime(2017, 4, 1, 13, 16, 32, 108);
string.Format("{0:y yy yyy yyyy}", dt); //17 17 2017 2017
string.Format("{0:M MM MMM MMMM}", dt); //4 04 四月 四月
string.Format("{0:d dd ddd dddd}", dt); //1 01 周六 星期六
string.Format("{0:t tt}", dt); //下 午下午
string.Format("{0:H HH}", dt); //13 13
string.Format("{0:h hh}", dt); //1 01
string.Format("{0:m mm}", dt); //16 16
string.Format("{0:s ss}", dt); //32 32string.Format("{0:z zz zzz}", dt); //+8 +08 +08:00
string.Format("{0:yyyy/MM/dd HH:mm:ss.fff}", dt); //2017/04/01 13:16:32.108
string.Format("{0:yyyy/MM/dd dddd}", dt); //2017/04/01 星期六
string.Format("{0:yyyy/MM/dd dddd tt hh:mm}", dt); //2017/04/01 星期六 下午 01:16
string.Format("{0:yyyyMMdd}", dt); //20170401
string.Format("{0:yyyy-MM-dd HH:mm:ss.fff}", dt); //2017-04-01 13:16:32.108
二、DateTime.ToString()
DateTime dt = new DateTime(2017,4,1,13,16,32,108);
dt.ToString("y yy yyy yyyy");//17 17 2017 2017
dt.ToString("M MM MMM MMMM");//4 04 四月 四月
dt.ToString("d dd ddd dddd");//1 01 周六 星期六
dt.ToString("t tt");//下 下午
dt.ToString("H HH");//13 13
dt.ToString("h hh");//1 01
dt.ToString("m mm");//16 16
dt.ToString("s ss");//32 32
dt.ToString("z zz zzz");//+8 +08 +08:00dt.ToString("yyyy/MM/dd HH:mm:ss.fff"); //2017/04/01 13:16:32.108
dt.ToString("yyyy/MM/dd dddd"); //2017/04/01 星期六
dt.ToString("yyyy/MM/dd dddd tt hh:mm"); //2017/04/01 星期六 下午 01:16
dt.ToString("yyyyMMdd"); //20170401
dt.ToString("yyyy-MM-dd HH:mm:ss.fff"); //2017-04-01 13:16:32.108
(八) 枚举(Enum)
枚举(Enum)在 C# 中是一种非常有用的数据类型,用于定义一组命名的常数。枚举使代码更具可读性和可维护性,尤其是当您有一组相关的常量需要在代码中使用时。
1. 枚举基础用法
一、声明枚举类型
在 C# 中,使用 enum
关键字来声明枚举类型。以下是声明枚举的一般语法:
enum <枚举类型的名称>
{ 是用逗号分隔的标识符列表,每个标识符代表一个枚举常量。
};
例如,定义一个表示星期几的枚举(在这个例子中,每个枚举常量的默认值从 0 开始递增。):
enum Days
{ Sun, // 0Mon, // 1Tue, // 2Wed, // 3Thu, // 4Fri, // 5Sat // 6
};
二、使用枚举常量
一旦定义了枚举类型,可以在代码中使用枚举常量来表示相关的值。例如:
class Program
{enum Days{Sun, Mon, Tue, Wed, Thu, Fri, Sat};static void Main(){Days today = Days.Wed;Console.WriteLine("今天是" + today); // WedConsole.WriteLine("数字是" + (int)today); // 3}
}
三、指定枚举常量的值
可以为枚举常量显式指定值。例如:
enum Months
{Jan = 1,Feb = 2,Mar = 3,Apr = 4,May = 5,Jun = 6,Jul = 7,Aug = 8,Sep = 9,Oct = 10,Nov = 11,Dec = 12
};
四、使用枚举类型
枚举类型可以作为方法的参数或返回值,从而增加代码的清晰度和可读性。例如,编写一个方法来检查是否是工作日:
enum Days { Sun, Mon, Tue, Wed, Thu, Fri, Sat };class Program
{static bool IsWeekday(Days day){return day != Days.Sat && day != Days.Sun;}static void Main(){Days today = Days.Mon;Console.WriteLine("今天是否是工作日:" + IsWeekday(today)); // 今天是否是工作日:True}
}
五、转换枚举类型
可以使用类型转换将枚举类型转换为整数值,或从整数值转换回枚举类型。例如:
enum Colors { Red, Green, Blue };class Program
{static void Main(){Colors myColor = Colors.Green;int colorValue = (int)myColor;Console.WriteLine("颜色数值: " + colorValue);// 颜色数值: 1int blueValue = 2;Colors anotherColor = (Colors)blueValue;Console.WriteLine("颜色: " + anotherColor); //颜色:Blue}
}
2. 枚举进阶使用
一、枚举的 Flags 属性
在需要表示位掩码(bitwise flags)的场景下,可以使用 Flags 枚举。这种枚举允许枚举常量的值是位掩码,允许按位组合多个枚举值。
[Flags] // 不加的话就会变成输出数字了
enum Permissions
{None = 0,Read = 1,Write = 2,Execute = 4
}class Program
{static void Main(){Permissions myPermissions = Permissions.Read | Permissions.Write;Console.WriteLine(myPermissions); // 输出: Read, Write}
}
二、使用 Enum.Parse 和 Enum.TryParse
这两个方法允许将字符串表示的枚举值转换为枚举类型。
enum Days { Sun, Mon, Tue, Wed, Thu, Fri, Sat };class Program
{static void Main(){string userInput = "Wed";Days day;// 这个方法有两个主要参数:输入的字符串和一个输出的枚举类型的变量。string output = Enum.TryParse(userInput, out day) ? "解析到的星期: " + day : "无效输入";Console.WriteLine(output);}
}
三、枚举在switch case中的使用
可以为枚举类型编写扩展方法,以便添加特定于枚举的功能。
enum Colors { Red, Green, Blue };static class ColorExtensions
{public static string ToFriendlyString(this Colors color){switch (color){case Colors.Red:return "红色";case Colors.Green:return "绿色";case Colors.Blue:return "蓝色";default:throw new ArgumentOutOfRangeException(nameof(color), color, null);}}
}class Program
{static void Main(){Colors myColor = Colors.Green;Console.WriteLine(myColor.ToFriendlyString()); // 输出: 绿色}
}
四、枚举的自定义属性
使用属性为枚举常量添加元数据,例如描述性字符串或其他有用的信息。(该例子使用了 反射 看不懂可以跳过哦)
using System;using System.ComponentModel;
using System.Reflection;public enum Weekday
{[Description("星期一")]Monday,[Description("星期二")]Tuesday,[Description("星期三")]Wednesday,[Description("星期四")]Thursday,[Description("星期五")]Friday,[Description("星期六")]Saturday,[Description("星期日")]Sunday}public static class EnumExtensions
{public static string GetDescription(Enum value){FieldInfo field = value.GetType() // 方法返回 value 的实际类型信息.GetField(value.ToString()); // 方法是反射中的一种,它从类型中获取指定名称的字段信息// 获取枚举值的描述信息(如果有的话)DescriptionAttribute[] attributes = (DescriptionAttribute[])value.GetType() // 获取枚举的类型信息.GetField(value.ToString()) // 获取枚举值对应的字段信息.GetCustomAttributes(typeof(DescriptionAttribute), false); // 获取字段上的描述属性if (attributes != null && attributes.Length > 0){return attributes[0].Description;}else{return value.ToString();}}
}class Program
{static void Main(string[] args){Weekday today = Weekday.Monday;Console.WriteLine($"今天是{EnumExtensions.GetDescription(today)}"); // 输出:今天是星期一}
}
三、类与对象
(一) 结构体(Struct)
当谈论 C# 编程语言中的结构体时,深入了解其定义、特性、使用场景以及与类的比较可以帮助我们更好地利用这一语言特性。
1. 结构体的定义和特性
在 C# 中,结构体是一种自定义的数据类型,使用 struct
关键字进行定义。结构体与类相似,但有几个关键区别:
- 值类型: 结构体是值类型,意味着它们的实例直接包含其数据,而不是通过引用访问。这种特性使得结构体更适合用来表示简单的数据结构,如坐标、颜色等。
- 字段和属性: 结构体可以包含字段、属性、方法和构造函数,用来定义结构体的数据和行为。
- 默认构造函数: 如果不定义任何构造函数,结构体会自动获得一个默认的无参构造函数,用来初始化字段。
public struct Point
{public int X;public int Y;public Point(int x, int y){X = x;Y = y;}
}
2. 类 vs 结构
类和结构在设计和使用时有不同的考虑因素,类适合表示复杂的对象和行为,支持继承和多态性,而结构则更适合表示轻量级数据和值类型,以提高性能并避免引用的管理开销。
- 值类型 / 引用类型:
- 结构是值类型(Value Type): 结构是值类型,它们在栈上分配内存,而不是在堆上。当将结构实例传递给方法或赋值给另一个变量时,将复制整个结构的内容。
- 类是引用类型(Reference Type): 类是引用类型,它们在堆上分配内存。当将类实例传递给方法或赋值给另一个变量时,实际上是传递引用(内存地址)而不是整个对象的副本。
- 继承和多态性:
-
- 结构不能继承: 结构不能继承其他结构或类,也不能作为其他结构或类的基类。
- 类支持继承: 类支持继承和多态性,可以通过派生新类来扩展现有类的功能。
- 默认构造函数:
- 结构不能有无参数的构造函数: 结构不能包含无参数的构造函数。每个结构都必须有至少一个有参数的构造函数。
- 类可以有无参数的构造函数: 类可以包含无参数的构造函数,如果没有提供构造函数,系统会提供默认的无参数构造函数。
- 赋值行为:
- 结构变量在赋值时会复制整个结构,因此每个变量都有自己的独立副本。
- 类型为类的变量在赋值时存储的是引用,因此两个变量指向同一个对象。
- 传递方式:
- 结构对象通常通过值传递,这意味着传递的是结构的副本,而不是原始结构对象本身。因此,在方法中对结构所做的更改不会影响到原始对象。
- 类型为类的对象在方法调用时通过引用传递,这意味着在方法中对对象所做的更改会影响到原始对象。
- 可空性:
- 结构体是值类型,不能直接设置为 null:因为 null 是引用类型的默认值,而不是值类型的默认值。如果你需要表示结构体变量的缺失或无效状态,可以使用 Nullable<T> 或称为 T? 的可空类型
- 类默认可为null: 类的实例默认可以为
null
,因为它们是引用类型。
- 性能和内存分配:
- 结构通常更轻量: 由于结构是值类型且在栈上分配内存,它们通常比类更轻量,适用于简单的数据表示
- 类可能有更多开销: 由于类是引用类型,可能涉及更多的内存开销和管理。
// 结构声明
struct MyStruct
{public int X;public int Y;// 结构不能有无参数的构造函数// public MyStruct() {}// 有参数的构造函数public MyStruct(int x, int y){X = x;Y = y;}// 结构不能继承// struct MyDerivedStruct : MyBaseStruct {}}// 类声明
class MyClass
{public int X;public int Y;// 类可以有无参数的构造函数public MyClass(){}// 有参数的构造函数public MyClass(int x, int y){X = x;Y = y;}// 类支持继承// class MyDerivedClass : MyBaseClass {}
}class Program
{static void Main(){// 结构是值类型,分配在栈上MyStruct structInstance1 = new MyStruct(1, 2);MyStruct structInstance2 = structInstance1; // 复制整个结构// 类是引用类型,分配在堆上MyClass classInstance1 = new MyClass(3, 4);MyClass classInstance2 = classInstance1; // 复制引用,指向同一个对象// 修改结构实例不影响其他实例structInstance1.X = 5;Console.WriteLine($"结构体1: {structInstance1.X}, {structInstance1.Y}"); // 5, 2Console.WriteLine($"结构体2: {structInstance2.X}, {structInstance2.Y}"); // 1, 2// 修改类实例会影响其他实例classInstance1.X = 6;Console.WriteLine($"类1: {classInstance1.X}, {classInstance1.Y}"); // 6, 4Console.WriteLine($"类2: {classInstance2.X}, {classInstance2.Y}"); // 6, 4}
}
(二) 类
在 C# 中,类使用 class
关键字进行定义。以下是一个简单的类的示例:
using System;public class Person
{// 字段public string Name;public int Age;// 构造函数public Person(string name, int age){Name = name;Age = age;}// 方法public void PrintDetails(){Console.WriteLine($"姓名: {Name}, 年龄: {Age}");}
}class Program
{static void Main(string[] args){// 创建 Person 对象并打印详细信息Person person = new Person("张三", 25);person1.PrintDetails();}
}
1. 类的特性
- 封装性(Encapsulation): 类通过将数据(字段)和方法封装在一起,隐藏实现细节,提供了更高的安全性和灵活性。
- 继承性(Inheritance): C# 支持类的继承,一个类可以从另一个类派生,从而可以重用基类的代码和属性。
- 多态性(Polymorphism): 允许不同类的对象对同一消息做出响应,提高了代码的灵活性和可维护性。
2. 构造方法
using System;public class Student
{// 私有字段private string name;private int age;// 无参构造函数public Student(){// 默认构造函数,初始化姓名和年龄为默认值name = "未设置";age = 0;}// 有参构造函数public Student(string name, int age){// 使用参数设置姓名和年龄this.name = name;this.age = age;}// 示例方法:打印学生信息public void PrintInfo(){Console.WriteLine($"姓名:{name},年龄:{age}");}
}public class Program
{public static void Main(){// 使用无参构造函数创建对象Student student1 = new Student();student1.PrintInfo(); // 输出:姓名:未设置,年龄:0// 使用有参构造函数创建对象Student student2 = new Student("张三", 20);student2.PrintInfo(); // 输出:姓名:张三,年龄:20}
}
3. 静态类 (Static Classes)
静态类是一种特殊的类,它只能包含静态成员(静态变量和静态方法),不能被实例化。通常用于组织一组相关的静态方法和静态变量。
static class Logger
{public static void LogError(string message){Console.WriteLine($"Error: {message}");}public static void LogInfo(string message){Console.WriteLine($"Info: {message}");}
}// 使用静态类示例
Logger.LogError("File not found."); // 调用静态方法 LogError()
Logger.LogInfo("User logged in."); // 调用静态方法 LogInfo()
在这个示例中,Logger
是一个静态类,它包含了 LogError()
和 LogInfo()
两个静态方法,可以直接通过类名调用。
一、静态变量
静态变量在类的所有实例中是共享的,它们只有一个副本存在于内存中。可以通过类名直接访问静态变量。
public class Student
{public string Name { get; set; }public static int Count { get; private set; } = 0; // 静态变量,用于统计学生数量public Student(string name){Name = name;Count++; // 每次创建学生对象时增加 Count}
}// 使用学生类示例
Student s1 = new Student("Alice");
Student s2 = new Student("Bob");// 共享同一个 Count 变量
Console.WriteLine(Student.Count); // 输出 2
二、静态方法
静态方法不依赖于类的实例,可以直接通过类名调用。它们通常用于实现与类相关但不依赖于特定对象实例的逻辑。
using System;public class 计算器
{// 静态方法:加、减public static int 加(int a, int b) { return a + b; }public static int 减(int a, int b) { return a - b; }public static void Main(string[] args){int a = 计算器.加(5, 3);Console.WriteLine($"和: {a}"); // 输出 和: 8int b = 计算器.减(10, 4);Console.WriteLine($"差: {b}"); // 输出 差: 6}
}
写到这里突然发现中文命名也可以,那我个人感觉中文是yyds,可以更好帮助我理解,这里就使用中文啦,嘿嘿嘿。
4. 析构函数
在C#中,析构函数的存在允许开发者在对象生命周期结束时执行清理工作,如释放非托管资源或执行其他清理操作,这对于程序性能和资源管理至关重要。
一、析构函数的基本概念
析构函数与类同名,前面加上一个波浪号(~
),如下所示:
class MyClass
{// 构造函数public MyClass(){// 构造函数的初始化工作}// 析构函数~MyClass(){// 析构函数的清理工作}
}
C#的析构函数不接受任何参数,也不能显示调用。它们由运行时自动调用,当对象即将被垃圾回收器回收时执行清理操作。
二、为什么需要析构函数?
主要有两个原因需要使用析构函数:
- 释放非托管资源: 如果类使用了非托管资源(如文件句柄、数据库连接、网络连接等),则需要在对象销毁时及时释放这些资源,以避免资源泄漏。
- 执行其他清理操作: 在对象生命周期结束时,可能需要执行一些额外的清理操作,比如日志记录、通知其他对象等。
在.NET开发中,通常建议依赖于.NET框架的自动垃圾回收机制,因为它能够有效地管理和释放不再使用的内存,避免了手动管理内存带来的复杂性和潜在的错误。但是,也有一些特定的场景可以考虑使用显式垃圾回收,这通常是为了优化性能或者确保资源的及时释放。
三、垃圾回收的使用场景
(一) 自动垃圾回收的场景
- 一般应用程序: 大多数.NET应用程序不需要显式调用垃圾回收。.NET框架的垃圾回收器会在必要时自动执行,并且通常能够有效地处理内存管理。
- 标准资源管理: 对于普通的资源管理,如对象的生命周期与其引用相关联,使用自动垃圾回收通常是最佳选择。这样可以避免手动管理资源带来的风险和复杂性。
- 一般性能需求: 大多数应用程序的性能要求可以通过自动垃圾回收器满足,不需要额外的手动控制。
(二) 显式垃圾回收的场景
- 特定资源释放需求: 如果你的应用程序使用了非托管资源(如文件句柄、数据库连接、网络连接等),这些资源的释放可能不受垃圾回收器的管理。在这种情况下,可以通过显式调用垃圾回收来确保及时释放这些资源。
- 性能优化: 在某些特定的性能优化场景中,显式调用垃圾回收可以帮助优化内存使用和减少不必要的内存占用。但是,这应该基于详细的性能分析和测试结果。
- 特殊内存管理需求: 在一些特殊的应用场景中,可能需要手动控制垃圾回收器的行为,以便更精确地管理内存分配和释放,或者确保在特定时间段内进行资源的释放。
四、*自动垃圾回收
using System;namespace LineApplication
{class Line{private double length; // 线条的长度public Line() // 构造函数{Console.WriteLine("对象已创建");}~Line() // 析构函数{Console.WriteLine("对象已删除");}public double Length // 属性封装线条长度{get { return length; }set { length = value; }}static void Main(string[] args){Line line = new Line(); // 创建 Line 对象line.Length = 6.0; // 设置线条长度Console.WriteLine("线条的长度: {0}", line.Length);}}
}
五、*显式垃圾回收
using System;
using System.IO;class FileWriter
{private StreamWriter writer;private string filePath;// 构造函数:初始化文件路径并打开文件流public FileWriter(string path){filePath = path;writer = new StreamWriter(filePath);Console.WriteLine($"打开文件 {filePath} 进行写入操作。");}// 写入文件方法public void WriteToFile(string message){writer.WriteLine(message);}// 析构函数:在对象被销毁时确保关闭文件流~FileWriter(){if (writer != null){writer.Close();Console.WriteLine($"关闭文件 {filePath}。");}}
}class Program
{static void Main(){// 创建 FileWriter 对象并写入数据FileWriter fileWriter = new FileWriter("test.txt");fileWriter.WriteToFile("Hello, world!");// 手动触发垃圾回收和析构函数调用,如果没有下面两句会报 Runtime Error (NZEC)GC.Collect(); // 这个方法强制执行垃圾回收器,尝试回收未使用的内存GC.WaitForPendingFinalizers(); // 这个方法会阻止当前线程,直到垃圾回收器完成其工作}
}/* 输出如下:
打开文件 test.txt 进行写入操作。
关闭文件 test.txt。
*/
(三) 封装
C# 封装根据具体的需要,设置使用者的访问权限,并通过 访问修饰符 来实现。一个 访问修饰符 定义了一个类成员的范围和可见性。C# 支持的访问修饰符如下所示:
在面向对象的编程中,封装是一个重要的概念。封装是指将数据和操作数据的方法组织在一起,隐藏其内部实现细节,只对外提供有限的接口。这样可以保护数据不被外部直接访问和修改,同时提高了代码的可维护性和可重用性。
在C#中,类是封装的基本单位。一个类包含了数据成员(字段)和方法。通过将字段设置为private,并将对字段的操作封装在public方法中,我们可以实现封装。
- 属性封装 提供了一种更现代、简洁的方式来定义类成员的读写行为,特别适合于需要在获取和设置数据时进行逻辑处理的情况。
- 显式方法封装 则更传统,对于简单的数据操作或者希望使用传统的方法访问字段的情况更为合适。
1. 使用属性封装方式
public class Student
{// 私有字段private string name;private int age;// 公共属性public string Name{get { return name; } // 获取姓名set { name = value; } // 设置姓名}public int Age{get { return age; } // 获取年龄set { age = value; } // 设置年龄}// 构造函数public Student(string name, int age){Name = name; // 使用属性来设置姓名Age = age; // 使用属性来设置年龄}// 公共方法public void SayHello(){Console.WriteLine($"你好,我的名字叫{name},我今年{age}岁。");}
}
使用属性设置和获取数据:
// 创建学生对象
Student student1 = new Student("张三", 20);// 使用属性设置姓名和年龄
student1.Name = "李四";
student1.Age = 22;// 使用属性获取姓名和年龄
string studentName = student1.Name;
int studentAge = student1.Age;// 调用公共方法
student1.SayHello();
2. 使用显式方法封装方式
现在,让我们看看显式方法封装的使用方式,使用之前的 Student
类定义:
public class Student
{// 私有字段private string name;private int age;// 构造函数public Student(string n, int a){SetName(n); // 使用显式方法设置姓名SetAge(a); // 使用显式方法设置年龄}// 显式的 set 方法public void SetName(string n) { name = n; }// 显式的 get 方法public string GetName() { return name; }// 显式的 set 方法,带有简单的验证public void SetAge(int a) { age = a; }// 显式的 get 方法public int GetAge() { return age; }// 公共方法public void SayHello(){Console.WriteLine($"你好,我的名字叫{GetName()},我今年{GetAge()}岁。");}
}
使用显式方法设置和获取数据:
// 创建学生对象
Student student2 = new Student("王五", 25);// 使用显式方法设置姓名和年龄
student2.SetName("赵六");
student2.SetAge(28);// 使用显式方法获取姓名和年龄
string studentName2 = student2.GetName();
int studentAge2 = student2.GetAge();// 调用公共方法
student2.SayHello();
(四) 继承
在面向对象编程(Object-Oriented Programming, OOP)中,继承是一种重要的概念,它允许我们创建一个新类(称为子类或派生类),从一个已存在的类(称为基类或父类)中继承属性和方法。
1. 基本概念
在C#中,继承通过使用 :
符号来实现。例如:
class 父类
{// 父类的成员和方法
}class 子类 : 父类
{// 子类的成员和方法,可以使用父类的成员和方法
}
这里的 子类
继承自 父类
,可以访问和使用 父类
中的公共和受保护的成员(方法和字段),并且可以添加新的成员或覆盖父类的方法以满足特定的需求。
2. 继承的优势
- 代码重用性: 继承允许您在不必重复编写相同代码的情况下,基于现有的类创建新的类。这样可以提高代码的可维护性和可读性。
- 派生类的特化: 派生类可以根据需要修改或添加新的功能,从而实现更具体的行为。这种灵活性使得代码可以更好地满足不同的业务需求。
- 维护性: 通过继承,当需要修改一些共享功能时,只需在父类中进行修改,所有继承自该父类的子类都会自动继承这些改变。
3. 关键特性
- 单继承: C# 中的类只支持单继承,即一个类只能直接继承自一个父类。这是为了避免多重继承可能带来的复杂性和冲突。
- 访问修饰符: 子类可以访问父类的
public
和protected
成员。private
成员只能在声明它们的类内部访问。 - 方法重写: 子类可以重写父类的虚方法或抽象方法,通过
override
关键字来实现新的方法体。
4. 示例
using System;// 父类
class Animal
{public void Eat() { Console.WriteLine("动物正在吃饭"); }
}// 子类
class Dog : Animal
{public void Bark() { Console.WriteLine("狗在汪汪叫"); }
}class Program
{static void Main(){Dog myDog = new Dog();myDog.Eat(); // 输出 "动物正在吃饭"myDog.Bark(); // 输出 "狗在汪汪叫"}
}
在这个示例中,Dog
类继承了 Animal
类。因此,Dog
类不仅可以调用自己新增的 Bark
方法,还可以调用 Animal
类中的 Eat
方法。
5. 接口(Interfaces)
接口定义了一组成员(方法、属性、事件和索引器),任何类可以实现这些接口,以确保它们具有相同的行为约定。接口在以下几个方面非常有用:
- 约定行为: 接口定义了类应该具备的方法和属性,强制了一种规范化的行为。
- 多态性: 类可以实现多个接口,从而使得一个类可以在不同的上下文中表现不同的行为,实现更大的灵活性。
- 解耦实现: 接口使得代码更加模块化,类的实现可以独立于接口的定义,从而提高了代码的可维护性和可测试性。
下面是一个简单的接口示例:
// 定义一个形状接口
public interface IShape
{// 计算面积的方法double CalculateArea();// 计算周长的方法double CalculatePerimeter();
}// 实现形状接口的矩形类
public class Rectangle : IShape
{// 矩形的宽度public double Width { get; set; }// 矩形的高度public double Height { get; set; }// 实现计算面积的方法public double CalculateArea(){return Width * Height;}// 实现计算周长的方法public double CalculatePerimeter(){return 2 * (Width + Height);}
}
在这个例子中,IShape
接口定义了所有形状类应该实现的方法 CalculateArea
和 CalculatePerimeter
。Rectangle
类实现了 IShape
接口,并提供了特定的实现来计算矩形的面积和周长。
6. 多重继承
C# 不支持类的多重继承(即一个类继承自多个类),这是为了避免多重继承可能带来的复杂性和歧义性。但是,C# 支持接口的多重继承,一个类可以实现多个接口,从而获得多态性的优势。
// 定义一个工作者接口
public interface IWorker
{void Work(); // 工作方法
}// 定义一个吃饭者接口
public interface IEater
{void Eat(); // 吃饭方法
}// 实现工作者和吃饭者接口的人类
public class Person : IWorker, IEater
{public void Work(){Console.WriteLine("人在工作。");}public void Eat(){Console.WriteLine("人在吃饭。");}
}
在这个例子中,Person
类实现了两个接口 IWorker
和 IEater
,因此它可以表现出工作和吃饭的行为。
(五) 多态(同一接口,多重表现)
在面向对象编程中,多态性是一个重要的概念,它使得相同的操作可以根据对象的不同表现出不同的行为。在 C# 中,多态性能够极大地提高代码的灵活性和可扩展性。
1. 什么是多态性?
多态性指的是同一个接口,具体实现可以有多种形式。简单来说,它允许不同类的对象对同一消息做出响应,但具体的行为取决于接收消息的对象的类型。这种能力使得代码可以根据具体的情况执行适当的操作,从而提高了代码的灵活性和可维护性。
2. 静态多态性与动态多态性
在 C# 中,多态性可以分为静态多态性(编译时多态)和动态多态性(运行时多态)两种形式:
- 静态多态性:也称为方法重载(overloading),是指在编译时根据方法的参数类型和个数来决定调用哪个重载方法。这种多态性是通过编译器静态决定的,不涉及运行时的对象类型。
- 动态多态性:也称为方法重写(overriding),是通过继承和虚方法实现的。在运行时,根据对象的实际类型来调用相应的方法,即使是通过基类引用访问子类对象,也能保证调用子类的方法。这种多态性更灵活,允许在运行时根据实际情况进行动态调整。
静态多态性(也称为编译时多态性)和动态多态性(运行时多态性)是面向对象编程中重要的概念,它们指的是不同层次上方法重载和方法重写的表现方式和时机。
一、静态多态性(重载)
静态多态性发生在编译时期,主要通过方法的重载来实现。在编译时,编译器根据调用方法时传递的参数类型、数量或顺序来决定调用哪个重载版本的方法。静态多态性是在编译阶段确定的,因此也称为早期绑定(early binding)。
- 方法重载是静态多态性的一种体现,它允许在同一个类中定义多个方法,方法名相同但参数列表不同。
- 重载决策在编译时期完成,根据方法的签名(参数类型、数量、顺序)来选择调用哪个方法。
public class Calculator
{public int Add(int a, int b) { return a + b; }public double Add(double a, double b) { return a + b; }
}class Program
{static void Main(){Calculator calc = new Calculator();int sum1 = calc.Add(3, 5); // 编译时决定调用 int Add(int a, int b)double sum2 = calc.Add(2.5, 3.7); // 编译时决定调用 double Add(double a, double b)}
}
二、动态多态性(重写)
动态多态性发生在运行时期,主要通过方法的重写来实现。当子类重写(override)父类的虚拟方法(virtual method)时,如果方法调用发生在父类引用指向子类对象的情况下,将根据实际的对象类型来决定调用哪个版本的方法。动态多态性是在运行时确定的,因此也称为晚期绑定(late binding)。
- 方法重写是动态多态性的一种体现,它允许子类重写父类中的虚拟方法,从而提供特定于子类的实现。
- 调用被重写方法时,会根据实际对象的类型决定调用的版本,而不是变量或引用的类型。
using System;// 定义一个动物基类
public class Animal
{public virtual void MakeSound(){Console.WriteLine("动物发出声音。");}
}// 定义一个狗类继承自动物类
public class Dog : Animal
{public override void MakeSound(){Console.WriteLine("狗发出汪汪的声音。");}
}// 定义一个猫类继承自动物类
public class Cat : Animal
{public override void MakeSound(){Console.WriteLine("猫发出喵喵的声音。");}
}// 代码运行
public class Program
{public static void Main(string[] args){Animal animal1 = new Dog();Animal animal2 = new Cat();animal1.MakeSound(); // 输出:狗发出汪汪的声音。animal2.MakeSound(); // 输出:猫发出喵喵的声音。}
}
在上述示例中,Animal
类的 MakeSound
方法被 Dog
和 Cat
类重写。在运行时,根据实际对象类型(Dog
或 Cat
),动态决定调用哪个重写版本的 MakeSound
方法。
3. 运算符重载
当我们在编写 C# 程序时,经常会遇到需要自定义类型进行运算的情况。C# 中提供了运算符重载(operator overloading)的机制,允许我们为自定义类型定义与内置类型相似的行为,从而提升代码的可读性和表达力。
一、运算符重载的语法
在 C# 中,运算符重载的语法如下所示:
public static <重载方法的返回类型> operator <要重载的运算符> (类型 操作数, 类型 操作数)
{// 运算符重载的具体实现return <返回结果>;
}
运算符 | 描述 |
| 一元运算符,可重载 |
| 二元运算符,可重载 |
| 比较运算符,可重载 |
| 条件逻辑运算符,不能直接重载 |
| 赋值运算符,不能重载 |
| 其他运算符,不能重载 |
二、示例:向量加法运算符重载
假设我们有一个向量类 ,我们希望能够使用 +
运算符对两个向量进行加法运算。我们可以这样定义运算符重载:
public class 向量
{public int X { get; set; } // X坐标属性public int Y { get; set; } // Y坐标属性// 构造函数,初始化向量的X和Y坐标public 向量(int x, int y){X = x;Y = y;}// 重载+运算符,实现向量的加法public static 向量 operator +(向量 v1, 向量 v2){return new 向量(v1.X + v2.X, v1.Y + v2.Y);}
}class Program
{static void Main(){向量 向量1 = new 向量(3, 5); // 创建向量1,坐标为(3, 5)向量 向量2 = new 向量(2, 7); // 创建向量2,坐标为(2, 7)向量 结果 = 向量1 + 向量2; // 使用重载的+运算符对向量进行相加,得到结果向量结果Console.WriteLine($"结果向量: ({结果.X}, {结果.Y})"); // 输出结果向量的坐标,例如:结果向量: (5, 12)}
}