一、提要
在 C# 教程的这一章中,我们继续描述 OOP。我们涵盖了接口、多态性、深浅复制、密封类和异常。
二、C# 接口
遥控器是观众和电视之间的接口。它是该电子设备的接口。外交礼仪指导外交领域的所有活动。道路规则是驾车者、骑自行车者和行人必须遵守的规则。
编程中的接口类似于前面的示例。接口就是:
- APIs
- Contracts
对象通过它们公开的方法与外部世界交互。实际的实现对程序员来说并不重要,或者它也可能是秘密的。一家公司可能会出售一个库,但它不想透露实际的实现。程序员可能会在 GUI 工具包的窗口上调用最大化方法,但对如何实现此方法一无所知。从这个角度来看,接口是对象与外部世界交互的方式,而不会过多地暴露其内部工作原理。
从第二个角度来看,接口就是契约。如果达成一致,则必须遵守。它们用于设计应用程序的架构。他们帮助组织代码。
接口是完全抽象的类型。它们是使用 interface 关键字声明的。接口只能具有方法、属性、事件或索引器的签名。所有接口成员都隐含地具有公共访问权限。接口成员不能指定访问修饰符。接口不能有完全实现的方法,也不能有成员字段。 C# 类可以实现任意数量的接口。一个接口也可以扩展任意数量的接口。实现接口的类必须实现接口的所有方法签名。
接口用于模拟多重继承。 C# 类只能从一个类继承,但它可以实现多个接口。使用接口的多重继承与继承方法和变量无关。它是关于继承由接口描述的想法或契约。
接口和抽象类之间有一个重要的区别。抽象类为继承层次结构中相关的类提供部分实现。另一方面,接口可以由彼此不相关的类实现。例如,我们有两个按钮。一个经典按钮和一个圆形按钮。两者都继承自一个为所有按钮提供一些通用功能的抽象按钮类。实现类是相关的,因为它们都是按钮。另一个示例可能有类 Database 和 SignIn。它们彼此不相关。我们可以应用一个 ILoggable 接口来强制他们创建一个方法来进行日志记录。
三、C#示例接口( interface)
下面的程序使用一个简单的界面。
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方法的实现。
四、多接口( multiple interfaces)
下一个示例展示如何实现多重接口。
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
六、 多态性( polymorphism)
多态性是以不同方式为不同数据输入使用运算符或函数的过程。实际上,多态性意味着,如果类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 classes)
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();
}
}
在上面的程序中,我们有一个基础数学课。这个类的唯一目的是为程序员提供一些有用的方法和常量。(在我们的例子中,为了简单起见,我们只有一个方法。)它不是为了继承而创建的。
为了防止未知情的其他程序员从该类派生,创建者将该类密封。如果您尝试编译此程序,会出现以下错误:“派生”无法从密封类型“数学”派生。
八、深拷贝和浅拷贝( deep copy vs shallow copy)
复制数据是编程中的一项重要任务。对象是OOP中的复合数据类型。对象中的成员字段可以通过值或引用存储。可以以两种方式执行复制。
浅拷贝将所有值和引用复制到新实例中。不复制引用所指向的数据;仅复制指针。新参照指向原始对象。对参照成员的任何更改都会影响两个对象。
深度副本将所有值复制到新实例中。对于存储为引用的成员,深度副本将执行被引用数据的深度副本。将创建引用对象的新副本。并且存储指向新创建的对象的指针。对这些引用对象的任何更改都不会影响对象的其他副本。深度副本是完全复制的对象。
如果成员字段是值类型,则执行字段的逐位复制。如果字段是引用类型,则复制引用,但不复制引用对象;因此,原始对象中的引用和克隆中的引用指向同一对象。
8.1 浅拷贝
下述代码示范浅拷贝。
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、大小)不会影响原始对象。更改引用对象(列)的成员也会影响原始对象。换句话说,两个对象在内存中引用相同的颜色对象。
8.2 深度拷贝( Deep copy)
为了改变这种行为,我们接下来进行深度复制。
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)。
九、异常( exceptions)
异常被设计用于处理异常的发生,异常是改变正常程序执行流程的特殊情况。引发或抛出异常。
在应用程序的执行过程中,许多事情可能会出错。磁盘可能已满,无法保存文件。当我们的应用程序尝试连接到站点时,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.
9.1 C# uncaught exception
当前上下文中任何未捕获的异常都会传播到更高的上下文,并寻找适当的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#编译器给出上述错误消息。
9.2 C# IOException
发生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
9.3 多重异常 Multiple exceptions
我们经常需要处理多个异常。
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.
9.4 客户异常 (custom exceptions)
自定义异常是用户定义的异常类,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