企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
# C# 面向对象编程 II > 原文: [https://zetcode.com/lang/csharp/oopii/](https://zetcode.com/lang/csharp/oopii/) 在 C# 教程的这一章中,我们将继续介绍 OOP。 我们介绍了接口,多态,深层和浅层副本,密封类和异常。 ## C# 接口 遥控器是观众和电视之间的接口。 它是此电子设备的接口。 外交礼仪指导外交领域的所有活动。 道路规则是驾车者,骑自行车者和行人必须遵守的规则。 编程中的接口类似于前面的示例。 接口是: * API * 合约 对象通过其公开的方法与外界交互。 实际的实现对程序员而言并不重要,或者也可能是秘密的。 公司可能会出售图书馆,但它不想透露实际的实现情况。 程序员可能会在 GUI 工具箱的窗口上调用`Maximize()`方法,但对如何实现此方法一无所知。 从这个角度来看,接口是对象与外界交互的方式,而又不会过多地暴露其内部功能。 从第二个角度来看,接口就是契约。 如果达成协议,则必须遵循。 它们用于设计应用的架构。 他们帮助组织代码。 接口是完全抽象的类型。 它们使用`interface`关键字声明。 接口只能具有方法,属性,事件或索引器的签名。 所有接口成员都隐式具有公共访问权限。 接口成员不能指定访问修饰符。 接口不能具有完全实现的方法,也不能具有成员字段。 C# 类可以实现任何数量的接口。 一个接口还可以扩展任何数量的接口。 实现接口的类必须实现接口的所有方法签名。 接口用于模拟多重继承。 C# 类只能从一个类继承,但可以实现多个接口。 使用接口的多重继承与继承方法和变量无关。 它是关于继承想法或合同的,这些想法或合同由接口描述。 接口和抽象类之间有一个重要的区别。 抽象类为继承层次结构中相关的类提供部分实现。 另一方面,可以通过彼此不相关的类来实现接口。 例如,我们有两个按钮。 经典按钮和圆形按钮。 两者都继承自抽象按钮类,该类为所有按钮提供了一些通用功能。 实现类是相关的,因为它们都是按钮。 另一个示例可能具有类`Database`和`SignIn`。 它们彼此无关。 我们可以应用`ILoggable`接口,该接口将迫使他们创建执行日志记录的方法。 ## C# 简单接口 以下程序使用一个简单的接口。 `Program.cs` ```cs using System; namespace SimpleInterface { interface IInfo { void DoInform(); } class Some : IInfo { public void DoInform() { Console.WriteLine("This is Some Class"); } } class Program { static void Main(string[] args) { var some = new Some(); some.DoInform(); } } } ``` 这是一个演示接口的简单 C# 程序。 ```cs interface IInfo { void DoInform(); } ``` 这是接口`IInfo`。 它具有`DoInform()`方法签名。 ```cs class Some : IInfo ``` 我们实现了`IInfo`接口。 为了实现特定的接口,我们使用冒号(:)运算符。 ```cs public void DoInform() { Console.WriteLine("This is Some Class"); } ``` 该类提供了`DoInform()`方法的实现。 ## C# 多个接口 下一个示例显示了一个类如何实现多个接口。 `Program.cs` ```cs using System; namespace MultipleInterfaces { interface Device { void SwitchOn(); void SwitchOff(); } interface Volume { void VolumeUp(); void VolumeDown(); } interface Pluggable { void PlugIn(); void PlugOff(); } class CellPhone : Device, Volume, Pluggable { public void SwitchOn() { Console.WriteLine("Switching on"); } public void SwitchOff() { Console.WriteLine("Switching on"); } public void VolumeUp() { Console.WriteLine("Volume up"); } public void VolumeDown() { Console.WriteLine("Volume down"); } public void PlugIn() { Console.WriteLine("Plugging In"); } public void PlugOff() { Console.WriteLine("Plugging Off"); } } class Program { static void Main(string[] args) { var cellPhone = new CellPhone(); cellPhone.SwitchOn(); cellPhone.VolumeUp(); cellPhone.PlugIn(); } } } ``` 我们有一个`CellPhone`类,它从三个接口继承。 ```cs class CellPhone : Device, Volume, Pluggable ``` 该类实现所有三个接口,并用逗号分隔。 `CellPhone`类必须实现来自所有三个接口的所有方法签名。 ```cs $ dotnet run Switching on Volume up Plugging In ``` 运行程序,我们得到此输出。 ## C# 多接口继承 下一个示例显示接口如何从多个其他接口继承。 `Program.cs` ```cs using System; namespace InterfaceInheritance { interface IInfo { void DoInform(); } interface IVersion { void GetVersion(); } interface ILog : IInfo, IVersion { void DoLog(); } class DBConnect : ILog { public void DoInform() { Console.WriteLine("This is DBConnect class"); } public void GetVersion() { Console.WriteLine("Version 1.02"); } public void DoLog() { Console.WriteLine("Logging"); } public void Connect() { Console.WriteLine("Connecting to the database"); } } class Program { static void Main(string[] args) { var db = new DBConnect(); db.DoInform(); db.GetVersion(); db.DoLog(); db.Connect(); } } } ``` 我们定义了三个接口。 我们可以按层次结构组织接口。 ```cs interface ILog : IInfo, IVersion ``` `ILog`接口继承自其他两个接口。 ```cs public void DoInform() { Console.WriteLine("This is DBConnect class"); } ``` `DBConnect`类实现`DoInform()`方法。 该方法由该类实现的`ILog`接口继承。 ```cs $ dotnet run This is DBConnect class Version 1.02 Logging Connecting to the database ``` 这是输出。 ## C# 多态 多态是以不同方式将运算符或函数用于不同数据输入的过程。 实际上,多态意味着如果类 B 从类 A 继承,那么它不必继承关于类 A 的所有内容。 它可以完成 A 类所做的某些事情。 通常,多态是以不同形式出现的能力。 从技术上讲,它是重新定义派生类的方法的能力。 多态与将特定实现应用于接口或更通用的基类有关。 多态是重新定义派生类的方法的能力。 `Program.cs` ```cs using System; namespace Polymorphism { abstract class Shape { protected int x; protected int y; public abstract int Area(); } class Rectangle : Shape { public Rectangle(int x, int y) { this.x = x; this.y = y; } public override int Area() { return this.x * this.y; } } class Square : Shape { public Square(int x) { this.x = x; } public override int Area() { return this.x * this.x; } } class Program { static void Main(string[] args) { Shape[] shapes = { new Square(5), new Rectangle(9, 4), new Square(12) }; foreach (Shape shape in shapes) { Console.WriteLine(shape.Area()); } } } } ``` 在上面的程序中,我们有一个抽象的`Shape`类。 此类演变为两个后代类别:`Rectangle`和`Square`。 两者都提供了自己的`Area()`方法实现。 多态为 OOP 系统带来了灵活性和可伸缩性。 ```cs public override int Area() { return this.x * this.y; } ... public override int Area() { return this.x * this.x; } ``` `Rectangle`和`Square`类具有`Area()`方法的自己的实现。 ```cs Shape[] shapes = { new Square(5), new Rectangle(9, 4), new Square(12) }; ``` 我们创建三个形状的数组。 ```cs foreach (Shape shape in shapes) { Console.WriteLine(shape.Area()); } ``` 我们遍历每个形状,并在其上调用`Area()`方法。 编译器为每种形状调用正确的方法。 这就是多态的本质。 ## C# 密封类 `sealed`关键字用于防止意外地从类派生。 密封类不能是抽象类。 `Program.cs` ```cs using System; namespace DerivedMath { sealed class Math { public static double GetPI() { return 3.141592; } } class Derived : Math { public void Say() { Console.WriteLine("Derived class"); } } class Program { static void Main(string[] args) { var dm = new Derived(); dm.Say(); } } } ``` 在上面的程序中,我们有一个`Math`基类。 该类的唯一目的是为程序员提供一些有用的方法和常量。 (出于简单起见,在我们的案例中,我们只有一种方法。)它不是从继承而创建的。 为了防止不知情的其他程序员从此类中派生,创建者创建了`sealed`类。 如果尝试编译该程序,则会出现以下错误:'Derived'不能从密封类'Math'派生。 ## C# 深层副本与浅层副本 数据复制是编程中的重要任务。 对象是 OOP 中的复合数据类型。 对象中的成员字段可以按值或按引用存储。 可以以两种方式执行复制。 浅表副本将所有值和引用复制到新实例中。 引用所指向的数据不会被复制; 仅指针被复制。 新的引用指向原始对象。 对引用成员的任何更改都会影响两个对象。 深层副本将所有值复制到新实例中。 如果成员存储为引用,则深层副本将对正在引用的数据执行深层副本。 创建一个引用对象的新副本。 并存储指向新创建对象的指针。 对这些引用对象的任何更改都不会影响该对象的其他副本。 深拷贝是完全复制的对象。 如果成员字段是值类型,则将对该字段进行逐位复制。 如果该字段是引用类型,则复制引用,但不是复制引用的对象。 因此,原始对象中的引用和克隆对象中的引用指向同一对象。 (来自 programmingcorner.blogspot.com 的明确解释) ### C# 浅表复制 以下程序执行浅表复制。 `Program.cs` ```cs using System; namespace ShallowCopy { class Color { public int red; public int green; public int blue; public Color(int red, int green, int blue) { this.red = red; this.green = green; this.blue = blue; } } class MyObject : ICloneable { public int id; public string size; public Color col; public MyObject(int id, string size, Color col) { this.id = id; this.size = size; this.col = col; } public object Clone() { return new MyObject(this.id, this.size, this.col); } public override string ToString() { var s = String.Format("id: {0}, size: {1}, color:({2}, {3}, {4})", this.id, this.size, this.col.red, this.col.green, this.col.blue); return s; } } class Program { static void Main(string[] args) { var col = new Color(23, 42, 223); var obj1 = new MyObject(23, "small", col); var obj2 = (MyObject) obj1.Clone(); obj2.id += 1; obj2.size = "big"; obj2.col.red = 255; Console.WriteLine(obj1); Console.WriteLine(obj2); } } } ``` 这是一个浅表副本的示例。 我们定义了两个自定义对象:`MyObject`和`Color`。 `MyObject`对象将引用 Color 对象。 ```cs class MyObject : ICloneable ``` 我们应该为要克隆的对象实现`ICloneable`接口。 ```cs public object Clone() { return new MyObject(this.id, this.size, this.col); } ``` `ICloneable`接口迫使我们创建`Clone()`方法。 此方法返回具有复制值的新对象。 ```cs var col = new Color(23, 42, 223); ``` 我们创建`Color`对象的实例。 ```cs var obj1 = new MyObject(23, "small", col); ``` 创建`MyObject`类的实例。 `Color`对象的实例传递给构造器。 ```cs var obj2 = (MyObject) obj1.Clone(); ``` 我们创建`obj1`对象的浅表副本,并将其分配给`obj2`变量。 `Clone()`方法返回`Object`,我们期望`MyObject`。 这就是我们进行显式转换的原因。 ```cs obj2.id += 1; obj2.size = "big"; obj2.col.red = 255; ``` 在这里,我们修改复制对象的成员字段。 我们增加`id`,将`size`更改为`"big"`,然后更改颜色对象的红色部分。 ```cs Console.WriteLine(obj1); Console.WriteLine(obj2); ``` `Console.WriteLine()`方法调用`obj2`对象的`ToString()`方法,该方法返回对象的字符串表示形式。 ```cs $ dotnet run id: 23, size: small, color:(255, 42, 223) id: 24, size: big, color:(255, 42, 223) ``` 我们可以看到 ID 是不同的(23 对 24)。 大小不同(`small`与`big`)。 但是,这两个实例的颜色对象的红色部分相同(255)。 更改克隆对象的成员值(`id`,`size`)不会影响原始对象。 更改引用对象(`col`)的成员也影响了原始对象。 换句话说,两个对象都引用内存中的同一颜色对象。 ### C# 深层复制 要更改此行为,我们接下来将做一个深层复制。 `Program.cs` ```cs using System; namespace DeepCopy { class Color : ICloneable { public int red; public int green; public int blue; public Color(int red, int green, int blue) { this.red = red; this.green = green; this.blue = blue; } public object Clone() { return new Color(this.red, this.green, this.blue); } } class MyObject : ICloneable { public int id; public string size; public Color col; public MyObject(int id, string size, Color col) { this.id = id; this.size = size; this.col = col; } public object Clone() { return new MyObject(this.id, this.size, (Color)this.col.Clone()); } public override string ToString() { var s = String.Format("id: {0}, size: {1}, color:({2}, {3}, {4})", this.id, this.size, this.col.red, this.col.green, this.col.blue); return s; } } class Program { static void Main(string[] args) { var col = new Color(23, 42, 223); var obj1 = new MyObject(23, "small", col); var obj2 = (MyObject) obj1.Clone(); obj2.id += 1; obj2.size = "big"; obj2.col.red = 255; Console.WriteLine(obj1); Console.WriteLine(obj2); } } } ``` 在此程序中,我们对对象执行深层复制。 ```cs class Color : ICloneable ``` 现在,`Color`类实现了`ICloneable`接口。 ```cs public object Clone() { return new Color(this.red, this.green, this.blue); } ``` 我们也为`Color`类提供了`Clone()`方法。 这有助于创建引用对象的副本。 ```cs public object Clone() { return new MyObject(this.id, this.size, (Color) this.col.Clone()); } ``` 当我们克隆`MyObject`时,我们根据`col`引用类型调用`Clone()`方法。 这样,我们也可以获得颜色值的副本。 ```cs $ dotnet run id: 23, size: small, color:(23, 42, 223) id: 24, size: big, color:(255, 42, 223) ``` 现在,所引用的`Color`对象的红色部分不再相同。 原始对象保留了其先前的值(23)。 ## C# 异常 异常是为处理异常的发生而设计的,这些特殊情况会改变程序执行的正常流程。 引发或引发异常。 在执行应用期间,许多事情可能出错。 磁盘可能已满,我们无法保存文件。 当我们的应用尝试连接到站点时,互联网连接可能会断开。 所有这些都可能导致我们的应用崩溃。 程序员有责任处理可以预期的错误。 `try`,`catch`和`finally`关键字用于处理异常。 `Program.cs` ```cs using System; namespace DivisionByZero { class Program { static void Main(string[] args) { int x = 100; int y = 0; int z; try { z = x / y; } catch (ArithmeticException e) { Console.WriteLine("An exception occurred"); Console.WriteLine(e.Message); } } } } ``` 在上面的程序中,我们有意将数字除以零。 这会导致错误。 ```cs try { z = x / y; } ``` 容易出错的语句放置在`try`块中。 ```cs catch (ArithmeticException e) { Console.WriteLine("An exception occurred"); Console.WriteLine(e.Message); } ``` 异常类型跟随`catch`关键字。 在我们的情况下,我们有一个`ArithmeticException`。 由于算术,转换或转换操作中的错误而引发此异常。 发生错误时,将执行`catch`关键字之后的语句。 发生异常时,将创建一个异常对象。 从该对象中,我们获得`Message`属性并将其打印到控制台。 ```cs $ dotnet run An exception occurred Attempted to divide by zero. ``` 代码示例的输出。 ### C# 未捕获的异常 当前上下文中任何未捕获的异常都会传播到更高的上下文,并寻找适当的`catch`块来处理它。 如果找不到任何合适的`catch`块,则 .NET 运行时的默认机制将终止整个程序的执行。 `Program.cs` ```cs using System; namespace UcaughtException { class Program { static void Main(string[] args) { int x = 100; int y = 0; int z = x / y; Console.WriteLine(z); } } } ``` 在此程序中,我们除以零。 没有自定义异常处理。 ```cs $ dotnet run Unhandled Exception: System.DivideByZeroException: Division by zero at UncaughtException.Main () [0x00000] ``` C# 编译器给出了以上错误消息。 ### C# `IOException` 发生 I/O 错误时,将抛出`IOException`。 在下面的示例中,我们读取文件的内容。 `Program.cs` ```cs using System; using System.IO; namespace ReadFile { class Program { static void Main(string[] args) { var fs = new FileStream("langs.txt", FileMode.OpenOrCreate); try { var sr = new StreamReader(fs); string line; while ((line = sr.ReadLine()) != null) { Console.WriteLine(line); } } catch (IOException e) { Console.WriteLine("IO Error"); Console.WriteLine(e.Message); } finally { Console.WriteLine("Inside finally block"); if (fs != null) { fs.Close(); } } } } } ``` 始终执行`finally`关键字之后的语句。 它通常用于清理任务,例如关闭文件或清除缓冲区。 ```cs } catch (IOException e) { Console.WriteLine("IO Error"); Console.WriteLine(e.Message); } ``` 在这种情况下,我们捕获了特定的`IOException`异常。 ```cs } finally { Console.WriteLine("Inside finally block"); if (fs != null) { fs.Close(); } } ``` 这些行确保关闭文件处理器。 ```cs $ cat langs.txt C# Java Python Ruby PHP JavaScript ``` 这些是`langs.txt`文件的内容。 ```cs $ dotnet run C# Java Python Ruby PHP JavaScript Inside finally block ``` 这是程序的输出。 我们使用`cat`命令和程序输出显示`langs`文件的内容。 ### C# 多个异常 我们经常需要处理多个异常。 `Program.cs` ```cs using System; using System.IO; namespace MultipleExceptions { class Program { static void Main(string[] args) { int x; int y; double z; try { Console.Write("Enter first number: "); x = Convert.ToInt32(Console.ReadLine()); Console.Write("Enter second number: "); y = Convert.ToInt32(Console.ReadLine()); z = x / y; Console.WriteLine("Result: {0:N} / {1:N} = {2:N}", x, y, z); } catch (DivideByZeroException e) { Console.WriteLine("Cannot divide by zero"); Console.WriteLine(e.Message); } catch (FormatException e) { Console.WriteLine("Wrong format of number."); Console.WriteLine(e.Message); } } } } ``` 在此示例中,我们捕获了各种异常。 请注意,更具体的异常应先于一般的异常。 我们从控制台读取两个数字,并检查零除错误和数字格式错误。 ```cs $ dotnet run Enter first number: we Wrong format of number. Input string was not in a correct format. ``` 运行示例,我们得到了这个结果。 ### C# 自定义异常 定制异常是从`System.Exception`类派生的用户定义的异常类。 `Program.cs` ```cs using System; namespace CustomException { class BigValueException : Exception { public BigValueException(string msg) : base(msg) { } } class Program { static void Main(string[] args) { int x = 340004; const int LIMIT = 333; try { if (x > LIMIT) { throw new BigValueException("Exceeded the maximum value"); } } catch (BigValueException e) { Console.WriteLine(e.Message); } } } } ``` 我们假定存在无法处理大量数字的情况。 ```cs class BigValueException : Exception ``` 我们有一个`BigValueException`类。 该类派生自内置的`Exception`类。 ```cs const int LIMIT = 333; ``` 大于此常数的数字在我们的程序中被视为`big`。 ```cs public BigValueException(string msg) : base(msg) {} ``` 在构造器内部,我们称为父级的构造器。 我们将消息传递给父级。 ```cs if (x > LIMIT) { throw new BigValueException("Exceeded the maximum value"); } ``` 如果该值大于限制,则抛出自定义异常。 我们给异常消息`Exceeded the maximum value`。 ```cs } catch (BigValueException e) { Console.WriteLine(e.Message); } ``` 我们捕获到异常并将其消息打印到控制台。 ```cs $ dotnet run Exceeded the maximum value ``` This is the output of the program. 在 C# 教程的这一部分中,我们继续讨论 C# 中的面向对象编程。