C#语法系统性学习
参考教程: 菜鸟教程
顶级语句
- 传统 C# 代码 - 在使用顶级语句之前,你必须像这样编写一个 C# 程序:
using System;
namespace MyApp
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
}
}
}
- 使用顶级语句的 C# 代码 - 使用顶级语句,可以简化为:
using System;
Console.WriteLine("Hello, World!");
- 顶级语句支持所有常见的 C# 语法,包括声明变量、定义方法、处理异常等。
using System;
using System.Linq;
// 顶级语句中的变量声明
int number = 42;
string message = "The answer to life, the universe, and everything is";
// 输出变量
Console.WriteLine($"{message} {number}.");
// 定义和调用方法
int Add(int a, int b) => a + b;
Console.WriteLine($"Sum of 1 and 2 is {Add(1, 2)}.");
// 使用 LINQ
var numbers = new[] { 1, 2, 3, 4, 5 };
var evens = numbers.Where(n => n % 2 == 0).ToArray();
Console.WriteLine("Even numbers: " + string.Join(", ", evens));
// 异常处理
try
{
int zero = 0;
int result = number / zero;
}
catch (DivideByZeroException ex)
{
Console.WriteLine("Error: " + ex.Message);
}
注意事项
- 文件限制:顶级语句只能在一个源文件中使用。如果在一个项目中有多个使用顶级语句的文件,会导致编译错误。
- 程序入口:如果使用顶级语句,则该文件会隐式地包含 Main 方法,并且该文件将成为程序的入口点。
- 作用域限制:顶级语句中的代码共享一个全局作用域,这意味着可以在顶级语句中定义的变量和方法可以在整个文件中访问。
顶级语句在简化代码结构、降低学习难度和加快开发速度方面具有显著优势,特别适合于编写简单程序和脚本。
List列表和Array相关
- 示例1
using System;
using System.Linq;
// 创建包含数字1-5的整数数组
var numbers = new[] { 1, 2, 3, 4, 5 };
// 使用LINQ的Where方法进行筛选,并将结果转换为数组
var evens = numbers.Where(n => n % 2 == 0).ToArray();
// 拆分数组为字符串并添加','分隔符
Console.WriteLine("Even numbers: " + string.Join(", ", evens));
数据类型
在 C# 中,变量分为以下几种类型:
- 值类型(Value types): 可以直接分配给一个值,比如
int - 引用类型(Reference types):不包含存储在变量中的实际数据,但它们包含对变量的引用
- 它们指的是一个内存位置。使用多个变量时,引用类型可以指向一个内存位置。如果内存位置的数据是由一个变量改变的,其他变量会自动反映这种值的变化。内置的 引用类型有:object、dynamic 和 string
- 指针类型(Pointer types)
类型转换
-
概念: 将一个
数据类型的值转换为另一个数据类型的过程-
隐式转换: 不需要编写代码来指定的转换,编译器会自动进行
- 将一个较小范围的数据类型转换为较大范围的数据类型时,编译器会自动完成类型转换,这些转换是 C# 默认的以安全方式进行的转换, 不会导致数据丢失(例如,从 int 到 long,从 float 到 double 等)
byte b = 10; int i = b; // 隐式转换,不需要显式转换 int intValue = 42; long longValue = intValue; // 隐式转换,从 int 到 long -
显式转换(强制类型转换)
- 是指将一个
较大范围的数据类型转换为较小范围的数据类型时,或者将一个对象类型转换为另一个对象类型时,需要使用强制类型转换符号进行显示转换,强制转换会造成数据丢失
int i = 10; byte b = (byte)i; // 显式转换,需要使用强制类型转换符号 double doubleValue = 3.14; int intValue = (int)doubleValue; // 强制从 double 到 int,数据可能损失小数部分 int intValue = 42; float floatValue = (float)intValue; // 强制从 int 到 float,数据可能损失精度 int intValue = 123; string stringValue = intValue.ToString(); // 将 int 转换为字符串-
C#提供了很多内置的类型转换方法(无法转换时,会自动抛异常)
- 例如** ToString,ToDouble,ToBoolean...**
string str = "123"; int number = Convert.ToInt32(str); // 转换成功,number为123 - 注意事项:字符串不是有效的整数表示,Convert.ToInt32 将抛出 FormatException
- 是指将一个
-
C# 还提供了多种类型转换方法,例如使用 Convert 类、Parse 方法和 TryParse 方法,这些方法可以帮助处理不同的数据类型之间的转换
string str = "123"; int num = Convert.ToInt32(str); string str = "123.45"; double d = double.Parse(str); string str = "123.45"; double d; bool success = double.TryParse(str, out d); if (success) { Console.WriteLine("转换成功: " + d); } else { Console.WriteLine("转换失败"); }- 自定义类型转换(暂时先了解一下即可)
-
变量
- 作用域---全局变量: 在整个类中可见,如果在命名空间级别定义,那么它们在整个命名空间中可见
class MyClass
{
int memberVar = 30; // 成员变量,在整个类中可见
}
- 作用域---静态变量: 在类级别上声明的,但它们的作用域也受限于其定义的类
class MyClass
{
static int staticVar = 40; // 静态变量,在整个类中可见
}
封装
-
概念: "把一个或多个项目封闭在一个物理的或者逻辑的包中,防止对实现细节的访问。
- C# 封装根据具体的需要,设置使用者的访问权限,并通过 访问修饰符 来实现。
-
一个 访问修饰符 *定义了一个类成员的范围和可见性。C# 支持的访问修饰符如下所示:
-
public:所有对象都可以访问;
-
private:对象本身在对象内部可以访问;
- 只有同一个类中的函数可以访问它的私有成员。即使是类的实例也不能访问它的私有成员。
using System; namespace RectangleApplication { class Rectangle { // 私有成员变量 private double length; private double width; // 公有方法,用于从用户输入获取矩形的长度和宽度 public void AcceptDetails() { Console.WriteLine("请输入长度:"); length = Convert.ToDouble(Console.ReadLine()); Console.WriteLine("请输入宽度:"); width = Convert.ToDouble(Console.ReadLine()); } // 公有方法,用于计算矩形的面积 public double GetArea() { return length * width; } // 公有方法,用于显示矩形的属性和面积 public void Display() { Console.WriteLine("长度: {0}", length); Console.WriteLine("宽度: {0}", width); Console.WriteLine("面积: {0}", GetArea()); } }//end class Rectangle class ExecuteRectangle { static void Main(string[] args) { // 创建 Rectangle 类的实例 Rectangle r = new Rectangle(); // 通过公有方法 AcceptDetails() 从用户输入获取矩形的长度和宽度 r.AcceptDetails(); // 通过公有方法 Display() 显示矩形的属性和面积 r.Display(); Console.ReadLine(); } } } 说明: - length 和 width 被声明为私有成员变量,以防止直接在类的外部访问和修改。 - AcceptDetails 方法允许用户输入矩形的长度和宽度,这是通过公有方法来操作私有变量 - 在 ExecuteRectangle 类中,通过创建 Rectangle 类的实例,然后调用其公有方法,来执行操作。这样,主程序无法直接访问和修改矩形的长度和宽度,而是通过类提供的公有接口来进行操作,实现了封装。 -
protected:只有该类对象及其子类对象可以访问(有助于实现继承,世袭制)
-
internal:同一个程序集的对象可以访问;
- 带有 internal 访问修饰符的任何成员可以被定义在该成员所定义的应用程序内的任何类或方法访问
using System; namespace RectangleApplication { class Rectangle { //成员变量 internal double length; internal double width; // 如果没有指定访问修饰符,则使用类成员的默认访问修饰符,即为 private double GetArea() { return length * width; } public void Display() { Console.WriteLine("长度: {0}", length); Console.WriteLine("宽度: {0}", width); Console.WriteLine("面积: {0}", GetArea()); } } class ExecuteRectangle { static void Main(string[] args) { Rectangle r = new Rectangle(); r.length = 4.5; r.width = 3.5; r.Display(); Console.ReadLine(); } } } -
protected internal:访问限于当前程序集或派生自包含类的类型。
-

按值传递和按引用传递的区别
using System;
namespace CalculatorApplication
{
class NumberManipulator
{
public void swap(int x, int y)
{
int temp;
temp = x; /* 保存 x 的值 */
x = y; /* 把 y 赋值给 x */
y = temp; /* 把 temp 赋值给 y */
}
static void Main(string[] args)
{
NumberManipulator n = new NumberManipulator();
/* 局部变量定义 */
int a = 100;
int b = 200;
Console.WriteLine("在交换之前,a 的值: {0}", a);
Console.WriteLine("在交换之前,b 的值: {0}", b);
/* 调用函数来交换值 */
n.swap(a, b);
Console.WriteLine("在交换之后,a 的值: {0}", a);
Console.WriteLine("在交换之后,b 的值: {0}", b);
Console.ReadLine();
}
}
}
- 结果如下:
在交换之前,a 的值:100
在交换之前,b 的值:200
在交换之后,a 的值:100
在交换之后,b 的值:200
using System;
namespace CalculatorApplication
{
class NumberManipulator
{
public void swap(ref int x, ref int y)
{
int temp;
temp = x; /* 保存 x 的值 */
x = y; /* 把 y 赋值给 x */
y = temp; /* 把 temp 赋值给 y */
}
static void Main(string[] args)
{
NumberManipulator n = new NumberManipulator();
/* 局部变量定义 */
int a = 100;
int b = 200;
Console.WriteLine("在交换之前,a 的值: {0}", a);
Console.WriteLine("在交换之前,b 的值: {0}", b);
/* 调用函数来交换值 */
n.swap(ref a, ref b);
Console.WriteLine("在交换之后,a 的值: {0}", a);
Console.WriteLine("在交换之后,b 的值: {0}", b);
Console.ReadLine();
}
}
}
- 结果如下:
在交换之前,a 的值:100
在交换之前,b 的值:200
在交换之后,a 的值:200
在交换之后,b 的值:100
第一段代码:按值传递(无法交换)
public void swap(int x, int y)
{
int temp;
temp = x; /* 保存 x 的值 */
x = y; /* 把 y 赋值给 x */
y = temp; /* 把 temp 赋值给 y */
}
问题:这段代码无法真正交换两个变量的值。
原因:
- 参数
x和y是按值传递的 - 方法内部操作的是原始变量的副本,而不是原始变量本身
- 方法执行完毕后,原始变量
a和b的值保持不变
输出结果:
在交换之前,a 的值: 100
在交换之前,b 的值: 200
在交换之后,a 的值: 100
在交换之后,b 的值: 200
第二段代码:按引用传递(成功交换)
public void swap(ref int x, ref int y)
{
int temp;
temp = x; /* 保存 x 的值 */
x = y; /* 把 y 赋值给 x */
y = temp; /* 把 temp 赋值给 y */
}
关键改进:使用了 ref 关键字
工作原理:
ref关键字表示按引用传递参数- 方法内部操作的是原始变量的内存地址,而不是副本
- 对参数的修改会直接影响原始变量
调用方式:
n.swap(ref a, ref b); // 调用时也需要加上 ref 关键字
输出结果:
在交换之前,a 的值: 100
在交换之前,b 的值: 200
在交换之后,a 的值: 200
在交换之后,b 的值: 100
核心概念总结
| 特性 | 按值传递 | 按引用传递 (ref) |
|---|---|---|
| 传递内容 | 变量的值副本 | 变量的内存地址 |
| 对原始变量的影响 | 无影响 | 直接影响 |
| 性能 | 需要复制数据 | 直接操作原数据 |
| 使用场景 | 不希望修改原始值 | 需要修改原始值 |
Python与C#的关键区别
| 特性 | C# | Python |
|---|---|---|
| 直接交换变量 | 需要临时变量和ref关键字 | a, b = b, a |
| 参数传递 | 明确区分值类型和引用类型 | 都是对象引用传递 |
| 修改原始变量 | 需要ref或out关键字 |
只能修改可变对象的内容 |
推荐:在Python中,直接使用 a, b = b, a 是最简单、最Pythonic的交换方式。
按输出传递参数(out参数应用)
- 以下代码展示了如何使用
out关键字在方法中返回一个值,并修改调用方的变量
using System;
namespace CalculatorApplication
{
class NumberManipulator
{
public void getValue(out int x )
{
int temp = 5;
x = temp;
}
static void Main(string[] args)
{
NumberManipulator n = new NumberManipulator();
/* 局部变量定义 */
int a = 100;
Console.WriteLine("在方法调用之前,a 的值: {0}", a);
/* 调用函数来获取值 */
n.getValue(out a);
Console.WriteLine("在方法调用之后,a 的值: {0}", a);
Console.ReadLine();
}
}
}
- 返回结果如下:
在方法调用之前,a 的值: 100
在方法调用之后,a 的值: 5
这段C#代码演示了输出参数(out parameter) 的使用。让我详细解释:
代码功能概述
这段代码展示了如何使用 out 关键字在方法中返回一个值,并修改调用方的变量。
方法定义部分
public void getValue(out int x)
{
int temp = 5;
x = temp; // 必须对 out 参数赋值
}
out int x:声明一个输出参数- 关键特性:在方法内部必须对
out参数进行赋值 x = temp:将局部变量temp的值赋给输出参数x
Main 方法部分
static void Main(string[] args)
{
NumberManipulator n = new NumberManipulator();
int a = 100; // 初始化变量 a
Console.WriteLine("在方法调用之前,a 的值: {0}", a); // 输出:100
n.getValue(out a); // 调用方法,传递 a 作为输出参数
Console.WriteLine("在方法调用之后,a 的值: {0}", a); // 输出:5
Console.ReadLine();
}
执行流程
- 初始化:变量
a被赋值为 100 - 方法调用前:输出
a = 100 - 方法调用:
n.getValue(out a)- 在
getValue方法中,x被赋值为 5 - 由于
x是out参数,这个赋值会直接修改原始变量a
- 在
- 方法调用后:输出
a = 5
输出结果
在方法调用之前,a 的值: 100
在方法调用之后,a 的值: 5
out 参数的特点
- 必须赋值:在方法内部必须对
out参数赋值 - 不关心初始值:调用方法前,变量的初始值不会被使用
- 引用传递:传递的是变量的引用,不是值的副本
- 调用语法:调用时必须使用
out关键字
与 ref 的区别
| 特性 | out 参数 |
ref 参数 |
|---|---|---|
| 初始化要求 | 调用前变量不需要初始化 | 调用前变量必须初始化 |
| 方法内赋值 | 必须赋值 | 可以选择性赋值 |
| 使用场景 | 用于从方法返回数据 | 用于传入和传出数据 |
Python 中的类似实现
在Python中,可以通过返回多个值或使用可变对象来模拟:
# 方式1:返回元组
def get_value():
temp = 5
return temp
a = 100
print(f"方法调用之前,a 的值: {a}") # 100
a = get_value() # 重新赋值
print(f"方法调用之后,a 的值: {a}") # 5
# 方式2:使用列表模拟引用
def get_value_out(container):
temp = 5
container[0] = temp
a_container = [100]
print(f"方法调用之前,a 的值: {a_container[0]}") # 100
get_value_out(a_container)
print(f"方法调用之后,a 的值: {a_container[0]}") # 5
这个例子很好地展示了C#中 out 参数的用法,常用于需要从方法返回多个值的场景。
C# 可空类型(Nullable)
-
可空类型就是允许值类型(如 int、bool、DateTime)可以有一个没有值的状态(即 null) -
在 C# 中,像 int、float、bool 等都是值类型(Value Types),默认情况下它们不能为 null。
例如以下赋值就会编译错误:
int a = null; // 编译错误但是使用 可空类型之后就可以:
int? a = null; // 合法,a此时的类型为Nullable<int>,int? 是 Nullable<int> 的语法糖(简写形式)。 int i; // 默认值为 0 int? ii; // 默认值为 null
可空引用类型 (Nullable Reference Types)
#nullable enable // 启用可空引用类型检查
// 传统引用类型(默认不可为空)
string nonNullableString = null; // 编译器警告!
string definitelyNotNull = "Hello"; // 正确
// 可空引用类型
string? nullableString = null; // 正确,明确声明可为null
// 使用时的检查
Console.WriteLine(nullableString.Length); // 编译器警告:可能为null
if (nullableString != null)
{
Console.WriteLine(nullableString.Length); // 正确,编译器知道不为null
}
总结
可空引用类型是C# 8.0引入的编译时静态分析特性,目的是:
- 减少
NullReferenceException - 让代码意图更明确
- 通过编译器警告提前发现问题
而可空值类型是运行时特性,真正允许值类型存储null值。
两者解决的问题不同,但都旨在提高代码的安全性和可读性。
结构体(轻量级的类)
- demo演示
using System;
using System.Text;
struct Books
{
public string title;
public string author;
public string subject;
public int book_id;
};
public class testStructure
{
public static void Main(string[] args)
{
Books Book1; /* 声明 Book1,类型为 Books */
Books Book2; /* 声明 Book2,类型为 Books */
/* book 1 详述 */
Book1.title = "C Programming";
Book1.author = "Nuha Ali";
Book1.subject = "C Programming Tutorial";
Book1.book_id = 6495407;
/* book 2 详述 */
Book2.title = "Telecom Billing";
Book2.author = "Zara Ali";
Book2.subject = "Telecom Billing Tutorial";
Book2.book_id = 6495700;
/* 打印 Book1 信息 */
Console.WriteLine( "Book 1 title : {0}", Book1.title);
Console.WriteLine("Book 1 author : {0}", Book1.author);
Console.WriteLine("Book 1 subject : {0}", Book1.subject);
Console.WriteLine("Book 1 book_id :{0}", Book1.book_id);
/* 打印 Book2 信息 */
Console.WriteLine("Book 2 title : {0}", Book2.title);
Console.WriteLine("Book 2 author : {0}", Book2.author);
Console.WriteLine("Book 2 subject : {0}", Book2.subject);
Console.WriteLine("Book 2 book_id : {0}", Book2.book_id);
Console.ReadKey();
}
}
类和结构有以下几个基本的不同点:
值类型 vs 引用类型:
- 结构是值类型(Value Type): 结构是值类型,它们在栈上分配内存,而不是在堆上。当将结构实例传递给方法或赋值给另一个变量时,将复制整个结构的内容。
- 类是引用类型(Reference Type): 类是引用类型,它们在堆上分配内存。当将类实例传递给方法或赋值给另一个变量时,实际上是传递引用(内存地址)而不是整个对象的副本。
继承和多态性:
- 结构不能继承: 结构不能继承其他结构或类,也不能作为其他结构或类的基类。
- 类支持继承: 类支持继承和多态性,可以通过派生新类来扩展现有类的功能。
默认构造函数:
- 结构不能有无参数的构造函数: 结构不能包含无参数的构造函数。
- 类可以有无参数的构造函数: 类可以包含无参数的构造函数,如果没有提供构造函数,系统会提供默认的无参数构造函数。
性能和内存分配:
- 结构通常更轻量: 由于结构是值类型且在栈上分配内存,它们通常比类更轻量,适用于简单的数据表示。
- 类可能有更多开销: 由于类是引用类型,可能涉及更多的内存开销和管理。
可以理解为轻量级的类吗?
是的,但需要谨慎理解这个比喻。
结构体确实可以被看作是"轻量级"的数据容器,但更重要的是理解它们的语义差异:
// 结构体 - 值语义
Point p1 = new Point(10, 20);
Point p2 = p1; // 复制值
p2.X = 30; // 不影响p1
// 类 - 引用语义
Customer c1 = new Customer { Name = "John" };
Customer c2 = c1; // 复制引用
c2.Name = "Jane"; // 影响c1,因为它们指向同一对象
实际使用建议
使用结构体的情况:
- 数据量小(通常小于16字节)
- 逻辑上表示一个值(如坐标、颜色)
- 不可变或很少修改
- 性能关键路径,需要避免GC压力
使用类的情况:
- 需要继承和多态
- 表示具有身份标识的实体
- 包含大量数据或复杂状态
- 需要共享引用语义
性能考虑
// 结构体数组 - 内存连续,缓存友好
Point[] points = new Point[1000]; // 内存中连续存储
// 类数组 - 存储引用,数据可能分散
Customer[] customers = new Customer[1000]; // 存储的是引用指针
总结:结构体确实是更轻量的数据容器,但选择使用结构体还是类应该基于语义需求(值语义vs引用语义)而不仅仅是性能考虑。在正确的场景中使用正确的类型,才能写出既高效又易于维护的代码。
枚举(Enum)
- 枚举是一组命名整型常量。枚举类型是使用 enum 关键字声明的
- C# 枚举是值类型。换句话说,枚举包含自己的值,且不能继承或传递继承。
- 枚举列表中的每个符号代表一个整数值,一个比它前面的符号大的整数值。默认情况下,第一个枚举符号的值是 0.例如:
enum Days { Sun, Mon, tue, Wed, thu, Fri, Sat };
using System;
public class EnumTest
{
enum Day { Sun, Mon, Tue, Wed, Thu, Fri, Sat };
static void Main()
{
int x = (int)Day.Sun;
int y = (int)Day.Fri;
Console.WriteLine("Sun = {0}", x); // Sun = 0
Console.WriteLine("Fri = {0}", y); // Fri = 5
}
}
析构函数,静态成员和静态函数
-
析构函数:用于资源清理(释放内存)
- 对象被销毁时自动调用的特殊方法,用于清理非托管资源
public class FileHandler { private FileStream fileStream; // 构造函数 public FileHandler(string filePath) { fileStream = new FileStream(filePath, FileMode.Open); } // 析构函数 ~FileHandler() { Console.WriteLine("析构函数被调用,清理资源"); fileStream?.Close(); } } -
静态成员:类级别的共享数据,所有实例共享
-
静态函数:工具方法,无需实例化即可调用
类的继承
- 一个
类可以继承多个接口,但只能继承自一个类。 派生类可以通过关键字base来调用基类的构造函数和方法。
class BaseClass
{
public void SomeMethod()
{
......
}
}
class DerivedClass : BaseClass
{
public void AnotherMethod()
{
// 调用父类的方法
base.SomeMethod();
......
}
}
- 演示面向对象的继承、方法重写和代码复用概念
using System;
var topTable = new Tabletop(10, 20);
//Console.WriteLine(topTable.GetCost());
topTable.Display();
class Rectangle
{
protected double length;
protected double width;
public Rectangle(double l, double w)
{
length = l;
width = w;
}
public double GetArea()
{
return length * width;
}
public virtual void Display() // 添加virtual允许重写
{
Console.WriteLine("长度: {0}", length);
Console.WriteLine("宽度: {0}", width);
Console.WriteLine("面积: {0}", GetArea());
}
}
class Tabletop : Rectangle
{
public Tabletop(double l, double w) : base(l, w) // 继承父类的构造方法
{
}
public double GetCost()
{
return GetArea() * 70;
}
public override void Display() // 重写父类的Display方法
{
base.Display();
Console.WriteLine("成本: {0}", GetCost());
}
}
- 程序运行结果:
长度: 10
宽度: 20
面积: 200
成本: 14000
-
子类继承父类的时候,私有的成员或者方法,不能被继承或者访问
- private成员只限类自己访问,是类的私有财产,即使是儿子也不行
public class Parent { private string privateField = "私有字段"; private void PrivateMethod() { Console.WriteLine("私有方法"); } public void ShowPrivate() { Console.WriteLine(privateField); // 只能在父类内部访问 PrivateMethod(); // 只能在父类内部调用 } } public class Child : Parent { public void TryAccessPrivate() { // 以下代码都会编译错误: // Console.WriteLine(privateField); // 错误:无法访问 // PrivateMethod(); // 错误:无法访问 } }- 封装的示例如下
public class Vehicle { public string model; private string _engineType; public string EngineType { get => _engineType; protected set => _engineType = value; // 子类可以设置,外部只读 } protected int _speed; public virtual int Speed { get => _speed; set => _speed = value >= 0 ? value : 0; // 添加验证逻辑 } } // 创建车辆对象 Vehicle car = new Vehicle(); // 可以设置(公共字段) car.model = "Toyota"; // 可以读取(公共属性) string engine = car.EngineType; // 不能设置(protected set) // car.EngineType = "V6"; // 编译错误 // 速度自动验证 car.Speed = 100; // 正常设置 car.Speed = -10; // 自动变为0很好的观察!你提出了一个重要的概念区分。EngineType和Speed都是属性(Property),不是方法(Method)。
属性和方法的区别
属性 (Property)
public string EngineType { get => _engineType; // 读取时执行 protected set => _engineType = value; // 写入时执行 }-
用途:封装字段的访问,像"智能字段"
-
调用方式:像字段一样直接使用
string type = car.EngineType; // 调用get // car.EngineType = "V6"; // 调用set (但这里protected) -
特点:通常不包含复杂逻辑,主要用于数据访问控制
方法 (Method)
public string GetEngineType() { return _engineType; } protected void SetEngineType(string value) { _engineType = value; }-
用途:执行操作或计算
-
调用方式:需要括号和参数
string type = car.GetEngineType(); // car.SetEngineType("V6"); -
特点:可以包含复杂逻辑,可以有返回值
为什么使用属性而不是方法?
属性提供了更自然的语法:
// 属性语法 (更简洁) car.Speed = 100; int currentSpeed = car.Speed; // 对应的方法语法 (更繁琐) car.SetSpeed(100); int currentSpeed = car.GetSpeed();属性在C#中的特殊地位:
- 可以被数据绑定
- 在Visual Studio中有智能感知支持
- 可以被序列化
- 有专门的元数据
总结
特性 属性(Property) 方法(Method) 语法 obj.Propertyobj.Method()用途 数据访问 执行操作 逻辑复杂度 简单验证/转换 可以很复杂 参数 通常无(set有value) 可以有多个 所以你的观察是正确的:
EngineType和Speed都是属性,它们使用特殊的get/set语法来提供字段般的访问体验,同时具备方法般的逻辑控制能力。 -
C#
多态性和抽象类的示例
using System;
namespace PolymorphismApplication
{
abstract class Shape
{
abstract public int area();
}
class Rectangle: Shape
{
private int length;
private int width;
public Rectangle( int a=0, int b=0)
{
length = a;
width = b;
}
public override int area ()
{
Console.WriteLine("Rectangle 类的面积:");
return (width * length);
}
}
class RectangleTester
{
static void Main(string[] args)
{
Rectangle r = new Rectangle(10, 7);
double a = r.area();
Console.WriteLine("面积: {0}",a);
Console.ReadKey();
}
}
}
代码结构概览
using System;
namespace PolymorphismApplication
{
// 代码分为三个主要部分
}
1. 抽象基类 Shape
abstract class Shape
{
abstract public int area();
}
abstract:表示这是一个抽象类- 不能直接实例化:
Shape s = new Shape();❌ 会报错 - 只能被其他类继承
- 不能直接实例化:
- 抽象方法
area():- 只有方法声明,没有方法体(没有
{}) - 强制所有子类必须实现这个方法
- 这是多态的基础
- 只有方法声明,没有方法体(没有
2. 具体子类 Rectangle
class Rectangle: Shape // 继承Shape
{
private int length;
private int width;
// 构造函数
public Rectangle(int a=0, int b=0)
{
length = a;
width = b;
}
// 实现抽象方法
public override int area()
{
Console.WriteLine("Rectangle 类的面积:");
return (width * length);
}
}
关键点:
- 继承关系:
Rectangle : Shape表示Rectangle继承自Shape - 构造函数参数默认值:
int a=0, int b=0允许不传参数创建对象 override:必须使用这个关键字来重写抽象方法- 具体实现:提供了计算矩形面积的具体逻辑
3. 测试类 RectangleTester
class RectangleTester
{
static void Main(string[] args)
{
Rectangle r = new Rectangle(10, 7); // 创建对象
double a = r.area(); // 调用方法
Console.WriteLine("面积: {0}",a); // 输出结果
Console.ReadKey(); // 等待按键
}
}
多态性体现
虽然这个简单例子没有完全展示多态,但扩展后可以这样:
// 多态的例子
Shape[] shapes = new Shape[3];
shapes[0] = new Rectangle(10, 7);
shapes[1] = new Circle(5); // 假设有Circle类
shapes[2] = new Triangle(4, 3); // 假设有Triangle类
foreach (Shape shape in shapes)
{
Console.WriteLine($"面积: {shape.area()}"); // 多态调用
}
程序输出
Rectangle 类的面积:
面积: 70
设计模式分析
模板方法模式:
- 基类定义"要做什么"(计算面积)
- 子类定义"具体怎么做"(如何计算矩形面积)
优点:
- 强制规范:确保所有形状都有area方法
- 扩展性好:可以轻松添加新的形状类
- 多态支持:可以用统一接口处理不同形状
这就是面向对象编程中抽象和多态的经典应用!
Vritual和abstract的区别
核心区别对比
| 特性 | virtual (虚方法) |
abstract (抽象方法) |
|---|---|---|
| 方法体 | 必须有实现 | 不能有实现 |
| 所在类 | 可以在普通类或抽象类中 | 只能在抽象类中 |
| 子类要求 | 可选重写 | 必须重写(除非子类也是抽象类) |
| 实例化 | 所在类可以实例化 | 所在类不能实例化 |
代码示例对比
使用 virtual 的例子:
class Animal
{
// virtual方法有默认实现
public virtual void MakeSound()
{
Console.WriteLine("动物发出声音");
}
}
class Dog : Animal
{
// 可选重写
public override void MakeSound()
{
Console.WriteLine("汪汪!");
}
}
class Cat : Animal
{
// 可以不重写,使用基类的实现
}
使用 abstract 的例子:
abstract class Shape
{
// abstract方法没有实现
public abstract double Area();
}
class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
// 必须重写
public override double Area()
{
return Width * Height;
}
}
class Circle : Shape
{
public double Radius { get; set; }
// 必须重写
public override double Area()
{
return Math.PI * Radius * Radius;
}
}
实际使用场景
使用 virtual 的情况:
- 方法有合理的默认实现
- 希望子类可选地定制行为
- 基类本身是有意义的实体
public class Vehicle
{
public virtual void StartEngine()
{
Console.WriteLine("启动发动机");
// 大多数车辆的启动逻辑相同
}
}
使用 abstract 的情况:
- 方法没有有意义的默认实现
- 强制要求子类提供特定实现
- 基类只是概念,不能独立存在
public abstract class DatabaseConnection
{
public abstract void Connect(); // 每种数据库连接方式完全不同
public abstract void Disconnect();
}
混合使用示例
abstract class PaymentProcessor
{
// 抽象方法:支付方式完全不同,必须由子类实现
public abstract void ProcessPayment(decimal amount);
// 虚方法:日志记录有默认实现,但子类可定制
public virtual void LogTransaction(decimal amount)
{
Console.WriteLine($"处理支付: {amount}");
}
// 普通方法:通用逻辑,不希望子类修改
public void ValidateAmount(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("金额必须大于0");
}
}
总结
virtual:"你可以改变我,如果你需要的话"abstract:"你必须实现我,因为我不知道该怎么做"
选择依据:
- 如果方法有有意义的默认行为 → 用
virtual - 如果方法必须由子类提供具体实现 → 用
abstract - 如果希望确保方法不被修改 → 都不用(普通方法)
多态实例演示
多态性允许我们使用基类类型的引用指向子类的对象,并且调用方法时会调用子类重写的方法。
using System;
using System.Collections.Generic;
public class Shape
{
public int X { get; private set; }
public int Y { get; private set; }
public int Height { get; set; }
public int Width { get; set; }
// 虚方法
public virtual void Draw()
{
Console.WriteLine("执行基类的画图任务");
}
}
class Circle : Shape
{
public override void Draw()
{
Console.WriteLine("画一个圆形");
base.Draw();
}
}
class Rectangle : Shape
{
public override void Draw()
{
Console.WriteLine("画一个长方形");
base.Draw();
}
}
class Triangle : Shape
{
public override void Draw()
{
Console.WriteLine("画一个三角形");
base.Draw();
}
}
class Program
{
static void Main(string[] args)
{
// 创建一个 List<Shape> 对象,并向该对象添加 Circle、Triangle 和 Rectangle
var shapes = new List<Shape>
{
new Rectangle(),
new Triangle(),
new Circle()
};
// 使用 foreach 循环对该列表的派生类进行循环访问,并对其中的每个 Shape 对象调用 Draw 方法
foreach (var shape in shapes)
{
shape.Draw();
}
Console.WriteLine("按下任意键退出。");
Console.ReadKey();
}
}
代码结构分析
1. 基类 Shape
public class Shape
{
public int X { get; private set; } // 只读属性
public int Y { get; private set; } // 只读属性
public int Height { get; set; } // 可读写属性
public int Width { get; set; } // 可读写属性
// 虚方法 - 可以被重写
public virtual void Draw()
{
Console.WriteLine("执行基类的画图任务");
}
}
特点:
- 包含基本的几何属性
Draw()方法标记为virtual,提供默认实现
2. 派生类(子类)
class Circle : Shape
{
public override void Draw()
{
Console.WriteLine("画一个圆形");
base.Draw(); // 调用基类方法
}
}
class Rectangle : Shape
{
public override void Draw()
{
Console.WriteLine("画一个长方形");
base.Draw(); // 调用基类方法
}
}
class Triangle : Shape
{
public override void Draw()
{
Console.WriteLine("画一个三角形");
base.Draw(); // 调用基类方法
}
}
关键点:
- 都使用
override重写Draw()方法 - 每个类添加自己特定的绘制逻辑
- 都调用
base.Draw()保留基类的通用逻辑
3. 多态性演示
class Program
{
static void Main(string[] args)
{
// 创建Shape类型的集合,但存储的是各种子类对象
var shapes = new List<Shape>
{
new Rectangle(),
new Triangle(),
new Circle()
};
// 多态调用:编译时类型是Shape,运行时调用具体子类的方法
foreach (var shape in shapes)
{
shape.Draw(); // 运行时决定调用哪个版本的Draw
}
}
}
多态性工作原理
编译时 vs 运行时
- 编译时类型:
Shape(编译器看到的类型) - 运行时类型:
Rectangle、Triangle、Circle(实际对象类型)
方法调用过程
shape.Draw(); // 这行代码在运行时:
// 1. 检查shape的实际类型
// 2. 调用该类型重写的Draw方法
// 3. 每个子类方法中又调用base.Draw()
程序输出结果
画一个长方形
执行基类的画图任务
画一个三角形
执行基类的画图任务
画一个圆形
执行基类的画图任务
按下任意键退出。
设计优势
1. 扩展性
// 可以轻松添加新形状,无需修改现有代码
class Hexagon : Shape
{
public override void Draw()
{
Console.WriteLine("画一个六边形");
base.Draw();
}
}
// 直接添加到列表中即可工作
shapes.Add(new Hexagon());
2. 统一处理
// 所有形状都可以用同样的方式处理
void ProcessShapes(List<Shape> shapes)
{
foreach (var shape in shapes)
{
shape.Draw(); // 绘制
// shape.Move(); // 移动(如果添加了此方法)
// shape.Rotate(); // 旋转(如果添加了此方法)
}
}
3. 代码复用
通过 base.Draw() 调用,所有子类都复用了基类的通用绘制逻辑。
实际应用场景
这种设计模式在GUI框架、游戏开发、图形处理等领域非常常见:
- GUI控件:所有控件继承自基类Control,都有Draw方法
- 游戏实体:所有游戏对象继承自GameObject,都有Update和Render方法
- 文档元素:所有文档元素都有统一的打印/显示接口
这就是面向对象编程中多态的强大之处:统一的接口,不同的实现!
又一个多态实例
- 通过参数多态来实现不同形状的面积计算
using System;
namespace PolymorphismApplication
{
class Shape
{
protected int width, height;
public Shape( int a=0, int b=0)
{
width = a;
height = b;
}
public virtual int area()
{
Console.WriteLine("父类的面积:");
return 0;
}
}
class Rectangle: Shape
{
public Rectangle( int a=0, int b=0): base(a, b)
{
}
public override int area ()
{
Console.WriteLine("Rectangle 类的面积:");
return (width * height);
}
}
class Triangle: Shape
{
public Triangle(int a = 0, int b = 0): base(a, b)
{
}
public override int area()
{
Console.WriteLine("Triangle 类的面积:");
return (width * height / 2);
}
}
class Caller
{
public void CallArea(Shape sh)
{
int a;
a = sh.area();
Console.WriteLine("面积: {0}", a);
}
}
class Tester
{
static void Main(string[] args)
{
Caller c = new Caller();
Rectangle r = new Rectangle(10, 7);
Triangle t = new Triangle(10, 5);
c.CallArea(r);
c.CallArea(t);
Console.ReadKey();
}
}
}
代码结构分析
1. 基类 Shape
class Shape
{
protected int width, height; // 受保护字段,子类可访问
// 构造函数,参数有默认值
public Shape(int a=0, int b=0)
{
width = a;
height = b;
}
// 虚方法,提供默认实现(返回0)
public virtual int area()
{
Console.WriteLine("父类的面积:");
return 0;
}
}
2. 派生类 Rectangle 和 Triangle
Rectangle 类:
class Rectangle: Shape
{
// 构造函数,调用基类构造函数
public Rectangle(int a=0, int b=0): base(a, b) { }
// 重写area方法,计算矩形面积
public override int area()
{
Console.WriteLine("Rectangle 类的面积:");
return (width * height);
}
}
Triangle 类:
class Triangle: Shape
{
// 构造函数,调用基类构造函数
public Triangle(int a=0, int b=0): base(a, b) { }
// 重写area方法,计算三角形面积
public override int area()
{
Console.WriteLine("Triangle 类的面积:");
return (width * height / 2);
}
}
3. 关键类 Caller - 体现多态性
class Caller
{
// 参数类型是基类Shape,但可以接受任何派生类对象
public void CallArea(Shape sh)
{
int a;
a = sh.area(); // 多态调用:运行时决定调用哪个area方法
Console.WriteLine("面积: {0}", a);
}
}
这是多态的核心体现:
- 方法参数是基类类型
Shape - 但可以传递任何派生类对象(
Rectangle、Triangle等) - 在运行时根据实际对象类型调用相应的方法
4. 测试类 Tester
class Tester
{
static void Main(string[] args)
{
Caller c = new Caller();
Rectangle r = new Rectangle(10, 7); // 创建矩形:宽10,高7
Triangle t = new Triangle(10, 5); // 创建三角形:底10,高5
c.CallArea(r); // 传递Rectangle对象
c.CallArea(t); // 传递Triangle对象
Console.ReadKey();
}
}
多态性工作原理
编译时类型 vs 运行时类型
// 在CallArea方法中:
public void CallArea(Shape sh) // 编译时类型:Shape
{
sh.area(); // 运行时根据sh的实际类型调用对应方法
}
方法调用过程
c.CallArea(r)→sh实际是Rectangle对象 → 调用Rectangle.area()c.CallArea(t)→sh实际是Triangle对象 → 调用Triangle.area()
程序输出结果
Rectangle 类的面积:
面积: 70
Triangle 类的面积:
面积: 25
设计优势分析
1. 扩展性极佳
// 可以轻松添加新形状,无需修改Caller类
class Circle : Shape
{
private int radius;
public Circle(int r) : base() { radius = r; }
public override int area()
{
Console.WriteLine("Circle 类的面积:");
return (int)(3.14 * radius * radius);
}
}
// 在Main方法中直接使用:
Circle circle = new Circle(5);
c.CallArea(circle); // 无需修改CallArea方法
2. 统一的接口
// 可以创建各种形状的集合统一处理
List<Shape> shapes = new List<Shape>
{
new Rectangle(10, 7),
new Triangle(10, 5),
new Circle(5)
};
foreach (Shape shape in shapes)
{
c.CallArea(shape); // 统一处理,自动调用正确的方法
}
3. 降低耦合
Caller类不需要知道具体有哪些形状类型- 新增形状类型时,
Caller类不需要任何修改 - 符合"开闭原则":对扩展开放,对修改关闭
实际应用场景
这种参数多态在设计模式中广泛应用:
- 策略模式:不同的算法实现同一接口
- 访问者模式:对不同类型的元素执行相同操作
- 命令模式:不同的命令对象有相同的执行接口
- 支付系统:不同的支付方式(支付宝、微信、银行卡)实现相同的支付接口
这个例子完美展示了面向对象编程中多态的核心价值:用统一的接口处理不同的对象,提高代码的灵活性和可维护性!

浙公网安备 33010602011771号