在 C# 教程的这一章中,我们继续描述 OOP。我们涵盖了接口、多态性、深浅复制、密封类和异常。
遥控器是观众和电视之间的接口。它是该电子设备的接口。外交礼仪指导外交领域的所有活动。道路规则是驾车者、骑自行车者和行人必须遵守的规则。
编程中的接口类似于前面的示例。接口就是:
对象通过它们公开的方法与外部世界交互。实际的实现对程序员来说并不重要,或者它也可能是秘密的。一家公司可能会出售一个库,但它不想透露实际的实现。程序员可能会在 GUI 工具包的窗口上调用最大化方法,但对如何实现此方法一无所知。从这个角度来看,接口是对象与外部世界交互的方式,而不会过多地暴露其内部工作原理。
从第二个角度来看,接口就是契约。如果达成一致,则必须遵守。它们用于设计应用程序的架构。他们帮助组织代码。
接口是完全抽象的类型。它们是使用 interface 关键字声明的。接口只能具有方法、属性、事件或索引器的签名。所有接口成员都隐含地具有公共访问权限。接口成员不能指定访问修饰符。接口不能有完全实现的方法,也不能有成员字段。 C# 类可以实现任意数量的接口。一个接口也可以扩展任意数量的接口。实现接口的类必须实现接口的所有方法签名。
接口用于模拟多重继承。 C# 类只能从一个类继承,但它可以实现多个接口。使用接口的多重继承与继承方法和变量无关。它是关于继承由接口描述的想法或契约。
接口和抽象类之间有一个重要的区别。抽象类为继承层次结构中相关的类提供部分实现。另一方面,接口可以由彼此不相关的类实现。例如,我们有两个按钮。一个经典按钮和一个圆形按钮。两者都继承自一个为所有按钮提供一些通用功能的抽象按钮类。实现类是相关的,因为它们都是按钮。另一个示例可能有类 Database 和 SignIn。它们彼此不相关。我们可以应用一个 ILoggable 接口来强制他们创建一个方法来进行日志记录。
下面的程序使用一个简单的界面。
Program.cs
- 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# 程序。
- interface IInfo
- {
- void DoInform();
- }
这是一个接口 IInfo。它具有 DoInform 方法签名。
class Some : IInfo
我们实现了 IInfo 接口。为了实现特定的接口,我们使用冒号 (:) 运算符。
- public void DoInform()
- {
- Console.WriteLine("This is Some Class");
- }
该类提供了doInfo方法的实现。
下一个示例展示如何实现多重接口。
Program.cs
- 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 类,它继承了三个接口.
class CellPhone : Device, Volume, Pluggable
该类实现了所有三个接口,它们由逗号分隔。CellPhone类必须实现所有三个接口的所有方法签名。
$ dotnet run Switching on Volume up Plugging In
下一个示例显示了接口如何从多个其他接口继承。
Program.cs
- 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();
- }
- }
我们定义了三个接口。我们可以在层次结构中组织接口。
interface ILog : IInfo, IVersion
ILog接口继承自其他两个接口。
- public void DoInform()
- {
- Console.WriteLine("This is DBConnect class");
- }
DBConnect类实现doInfo方法。该方法由类实现的ILog接口继承。
- $ dotnet run
- This is DBConnect class
- Version 1.02
- Logging
- Connecting to the database
多态性是以不同方式为不同数据输入使用运算符或函数的过程。实际上,多态性意味着,如果类B继承自类A,则它不必继承关于类A的一切;它可以做一些A类不同的事情。
一般来说,多态性是以不同形式出现的能力。从技术上讲,它是为派生类重新定义方法的能力。多态性与特定实现应用于接口或更通用的基类有关。
多态性是为派生类重新定义方法的能力。
Program.cs
- 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());
- }
- }
- }
在上面的程序中,我们有一个抽象的形状类。该类变形为两个子类:矩形和正方形。两者都提供了各自的面积方法实现。多态性为OOP系统带来了灵活性和可伸缩性。
- public override int Area()
- {
- return this.x * this.y;
- }
- ...
- public override int Area()
- {
- return this.x * this.x;
- }
矩形和正方形类有各自的面积方法实现。
Shape[] shapes = { new Square(5), new Rectangle(9, 4), new Square(12) };
我们创建三个形状的数组。
- foreach (Shape shape in shapes)
- {
- Console.WriteLine(shape.Area());
- }
我们遍历每个形状并调用其上的面积方法。编译器为每个形状调用正确的方法。这就是多态性的本质。
sealed关键字用于防止从类中意外派生。密封类不能是抽象类。
Program.cs
- 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();
- }
- }
在上面的程序中,我们有一个基础数学课。这个类的唯一目的是为程序员提供一些有用的方法和常量。(在我们的例子中,为了简单起见,我们只有一个方法。)它不是为了继承而创建的。
为了防止未知情的其他程序员从该类派生,创建者将该类密封。如果您尝试编译此程序,会出现以下错误:“派生”无法从密封类型“数学”派生。
复制数据是编程中的一项重要任务。对象是OOP中的复合数据类型。对象中的成员字段可以通过值或引用存储。可以以两种方式执行复制。
浅拷贝将所有值和引用复制到新实例中。不复制引用所指向的数据;仅复制指针。新参照指向原始对象。对参照成员的任何更改都会影响两个对象。
深度副本将所有值复制到新实例中。对于存储为引用的成员,深度副本将执行被引用数据的深度副本。将创建引用对象的新副本。并且存储指向新创建的对象的指针。对这些引用对象的任何更改都不会影响对象的其他副本。深度副本是完全复制的对象。
如果成员字段是值类型,则执行字段的逐位复制。如果字段是引用类型,则复制引用,但不复制引用对象;因此,原始对象中的引用和克隆中的引用指向同一对象。
下述代码示范浅拷贝。
Program.cs
- 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对象将具有对颜色对象的引用。
class MyObject : ICloneable
我们应该为将要克隆的对象实现ICloneable接口。
public object Clone()
{
return new MyObject(this.id, this.size, this.col);
}
Cloneable接口迫使我们创建克隆方法。此方法返回具有复制值的新对象。
var col = new Color(23, 42, 223);
我们创建颜色对象的实例。
var obj1 = new MyObject(23, "small", col);
创建MyObject类的实例。颜色对象的实例被传递给构造函数。
var obj2 = (MyObject) obj1.Clone();
我们创建obj1对象的浅拷贝,并将其分配给obj2变量。克隆方法返回一个对象,我们期望MyObject。这就是我们进行显式转换的原因。
- obj2.id += 1;
- obj2.size = "big";
- obj2.col.red = 255;
这里我们修改复制对象的成员字段。我们增加id,将大小更改为“大”,并更改颜色对象的红色部分。
Console.WriteLine(obj1); Console.WriteLine(obj2);
控制台。WriteLine方法调用obj2对象的ToString方法,该方法返回对象的字符串表示。
$ dotnet run id: 23, size: small, color:(255, 42, 223) id: 24, size: big, color:(255, 42, 223)
我们可以看到ID是不同的(23对24)。大小不同(“小”与“大”)。但是颜色对象的红色部分对于两个实例(255)是相同的。更改克隆对象的成员值(id、大小)不会影响原始对象。更改引用对象(列)的成员也会影响原始对象。换句话说,两个对象在内存中引用相同的颜色对象。
为了改变这种行为,我们接下来进行深度复制。
Program.cs
- 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);
- }
- }
在这个程序中,我们对对象执行深度复制
class Color : ICloneable
现在,Color类实现了ICloneable接口。
- public object Clone()
- {
- return new Color(this.red, this.green, this.blue);
- }
我们也为颜色类提供了一个克隆方法。这有助于创建引用对象的副本。
- public object Clone()
- {
- return new MyObject(this.id, this.size,
- (Color) this.col.Clone());
- }
克隆MyObject时,我们调用col引用类型的克隆方法。这样,我们也有一个颜色值的副本。
$ dotnet run id: 23, size: small, color:(23, 42, 223) id: 24, size: big, color:(255, 42, 223)
现在,引用颜色对象的红色部分不相同。原始对象保留了其先前的值(23)。
异常被设计用于处理异常的发生,异常是改变正常程序执行流程的特殊情况。引发或抛出异常。
在应用程序的执行过程中,许多事情可能会出错。磁盘可能已满,无法保存文件。当我们的应用程序尝试连接到站点时,Internet连接可能会中断。所有这些都可能导致应用程序崩溃。程序员有责任处理可以预期的错误。
try、catch和finally关键字用于处理异常。
Program.cs
int x = 100;
int y = 0;
int z;
try
{
z = x / y;
}
catch (ArithmeticException e)
{
Console.WriteLine("An exception occurred");
Console.WriteLine(e.Message);
}
在上面的程序中,我们有意将数字除以零。这会导致错误。
try
{
z = x / y;
}
容易出错的语句放在try块中。
catch (ArithmeticException e)
{
Console.WriteLine("An exception occurred");
Console.WriteLine(e.Message);
}
异常类型跟随catch关键字。在我们的例子中,我们有一个算术例外。此异常是由于算术、转换或转换操作中的错误而引发的。发生错误时,将执行catch关键字后面的语句。发生异常时,将创建异常对象。从这个对象中,我们获得消息属性并将其打印到控制台。
$ dotnet run An exception occurred Attempted to divide by zero.
当前上下文中任何未捕获的异常都会传播到更高的上下文,并寻找适当的catch块来处理它。如果找不到任何合适的catch块,.NET运行时的默认机制将终止整个程序的执行。
Program.cs
int x = 100; int y = 0; int z = x / y; Console.WriteLine(z);
在这个程序中,我们除以零。没有自定义异常处理。
$ dotnet run Unhandled exception. System.DivideByZeroException: Attempted to divide by zero. ...
C#编译器给出上述错误消息。
发生I/O错误时引发IOException。在下面的示例中,我们读取文件的内容。
Program.cs
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关键字后面的语句。它通常用于清理任务,如关闭文件或清除缓冲区。
}
catch (IOException e)
{
Console.WriteLine("IO Error");
Console.WriteLine(e.Message);
}
在这种情况下,我们捕获特定的IOException异常。
}
finally
{
Console.WriteLine("Inside finally block");
if (fs != null)
{
fs.Close();
}
}
这些行保证文件处理程序已关闭。
$ cat langs.txt C# Java Python Ruby PHP JavaScript
这些是langs.txt文件的内容。
$ dotnet run C# Java Python Ruby PHP JavaScript Inside finally block
我们经常需要处理多个异常。
Program.cs
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);
}
在本例中,我们捕获了各种异常。请注意,更具体的例外应先于一般例外。我们从控制台读取两个数字,并检查零除法错误和错误的数字格式。
$ dotnet run Enter first number: we Wrong format of number. Input string was not in a correct format.
自定义异常是用户定义的异常类,System.Exception 类继承.
Program.cs
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);
}
class BigValueException : Exception
{
public BigValueException(string msg) : base(msg) { }
}
我们假设我们有一个无法处理大数字的情况。
class BigValueException : Exception
我们有一个BigValueException类。此类派生自内置异常类。
const int LIMIT = 333;
大于该常数的数字被我们的程序视为“大”。
public BigValueException(string msg) : base(msg) {}
在构造函数中,我们调用父构造函数。我们将消息传递给家长。
if (x > LIMIT)
{
throw new BigValueException("Exceeded the maximum value");
}
如果值大于限制,则抛出自定义异常。我们给异常一条消息“超出最大值”。
}
catch (BigValueException e)
{
Console.WriteLine(e.Message);
}
我们捕获异常并将其消息打印到控制台。
$ dotnet run Exceeded the maximum value