原文: https://zetcode.com/lang/csharp/oopii/

在 C# 教程的这一章中,我们将继续介绍 OOP。 我们介绍了接口,多态,深层和浅层副本,密封类和异常。

C# 接口

遥控器是观众和电视之间的接口。 它是此电子设备的接口。 外交礼仪指导外交领域的所有活动。 道路规则是驾车者,骑自行车者和行人必须遵守的规则。 编程中的接口类似于前面的示例。

接口是:

  • API
  • 合约

对象通过其公开的方法与外界交互。 实际的实现对程序员而言并不重要,或者也可能是秘密的。 公司可能会出售图书馆,但它不想透露实际的实现情况。 程序员可能会在 GUI 工具箱的窗口上调用Maximize()方法,但对如何实现此方法一无所知。 从这个角度来看,接口是对象与外界交互的方式,而又不会过多地暴露其内部功能。

从第二个角度来看,接口就是契约。 如果达成协议,则必须遵循。 它们用于设计应用的架构。 他们帮助组织代码。

接口是完全抽象的类型。 它们使用interface关键字声明。 接口只能具有方法,属性,事件或索引器的签名。 所有接口成员都隐式具有公共访问权限。 接口成员不能指定访问修饰符。 接口不能具有完全实现的方法,也不能具有成员字段。 C# 类可以实现任何数量的接口。 一个接口还可以扩展任何数量的接口。 实现接口的类必须实现接口的所有方法签名。

接口用于模拟多重继承。 C# 类只能从一个类继承,但可以实现多个接口。 使用接口的多重继承与继承方法和变量无关。 它是关于继承想法或合同的,这些想法或合同由接口描述。

接口和抽象类之间有一个重要的区别。 抽象类为继承层次结构中相关的类提供部分实现。 另一方面,可以通过彼此不相关的类来实现接口。 例如,我们有两个按钮。 经典按钮和圆形按钮。 两者都继承自抽象按钮类,该类为所有按钮提供了一些通用功能。 实现类是相关的,因为它们都是按钮。 另一个示例可能具有类DatabaseSignIn。 它们彼此无关。 我们可以应用ILoggable接口,该接口将迫使他们创建执行日志记录的方法。

C# 简单接口

以下程序使用一个简单的接口。

Program.cs

  1. using System;
  2. namespace SimpleInterface
  3. {
  4. interface IInfo
  5. {
  6. void DoInform();
  7. }
  8. class Some : IInfo
  9. {
  10. public void DoInform()
  11. {
  12. Console.WriteLine("This is Some Class");
  13. }
  14. }
  15. class Program
  16. {
  17. static void Main(string[] args)
  18. {
  19. var some = new Some();
  20. some.DoInform();
  21. }
  22. }
  23. }

这是一个演示接口的简单 C# 程序。

  1. interface IInfo
  2. {
  3. void DoInform();
  4. }

这是接口IInfo。 它具有DoInform()方法签名。

  1. class Some : IInfo

我们实现了IInfo接口。 为了实现特定的接口,我们使用冒号(:)运算符。

  1. public void DoInform()
  2. {
  3. Console.WriteLine("This is Some Class");
  4. }

该类提供了DoInform()方法的实现。

C# 多个接口

下一个示例显示了一个类如何实现多个接口。

Program.cs

  1. using System;
  2. namespace MultipleInterfaces
  3. {
  4. interface Device
  5. {
  6. void SwitchOn();
  7. void SwitchOff();
  8. }
  9. interface Volume
  10. {
  11. void VolumeUp();
  12. void VolumeDown();
  13. }
  14. interface Pluggable
  15. {
  16. void PlugIn();
  17. void PlugOff();
  18. }
  19. class CellPhone : Device, Volume, Pluggable
  20. {
  21. public void SwitchOn()
  22. {
  23. Console.WriteLine("Switching on");
  24. }
  25. public void SwitchOff()
  26. {
  27. Console.WriteLine("Switching on");
  28. }
  29. public void VolumeUp()
  30. {
  31. Console.WriteLine("Volume up");
  32. }
  33. public void VolumeDown()
  34. {
  35. Console.WriteLine("Volume down");
  36. }
  37. public void PlugIn()
  38. {
  39. Console.WriteLine("Plugging In");
  40. }
  41. public void PlugOff()
  42. {
  43. Console.WriteLine("Plugging Off");
  44. }
  45. }
  46. class Program
  47. {
  48. static void Main(string[] args)
  49. {
  50. var cellPhone = new CellPhone();
  51. cellPhone.SwitchOn();
  52. cellPhone.VolumeUp();
  53. cellPhone.PlugIn();
  54. }
  55. }
  56. }

我们有一个CellPhone类,它从三个接口继承。

  1. class CellPhone : Device, Volume, Pluggable

该类实现所有三个接口,并用逗号分隔。 CellPhone类必须实现来自所有三个接口的所有方法签名。

  1. $ dotnet run
  2. Switching on
  3. Volume up
  4. Plugging In

运行程序,我们得到此输出。

C# 多接口继承

下一个示例显示接口如何从多个其他接口继承。

Program.cs

  1. using System;
  2. namespace InterfaceInheritance
  3. {
  4. interface IInfo
  5. {
  6. void DoInform();
  7. }
  8. interface IVersion
  9. {
  10. void GetVersion();
  11. }
  12. interface ILog : IInfo, IVersion
  13. {
  14. void DoLog();
  15. }
  16. class DBConnect : ILog
  17. {
  18. public void DoInform()
  19. {
  20. Console.WriteLine("This is DBConnect class");
  21. }
  22. public void GetVersion()
  23. {
  24. Console.WriteLine("Version 1.02");
  25. }
  26. public void DoLog()
  27. {
  28. Console.WriteLine("Logging");
  29. }
  30. public void Connect()
  31. {
  32. Console.WriteLine("Connecting to the database");
  33. }
  34. }
  35. class Program
  36. {
  37. static void Main(string[] args)
  38. {
  39. var db = new DBConnect();
  40. db.DoInform();
  41. db.GetVersion();
  42. db.DoLog();
  43. db.Connect();
  44. }
  45. }
  46. }

我们定义了三个接口。 我们可以按层次结构组织接口。

  1. interface ILog : IInfo, IVersion

ILog接口继承自其他两个接口。

  1. public void DoInform()
  2. {
  3. Console.WriteLine("This is DBConnect class");
  4. }

DBConnect类实现DoInform()方法。 该方法由该类实现的ILog接口继承。

  1. $ dotnet run
  2. This is DBConnect class
  3. Version 1.02
  4. Logging
  5. Connecting to the database

这是输出。

C# 多态

多态是以不同方式将运算符或函数用于不同数据输入的过程。 实际上,多态意味着如果类 B 从类 A 继承,那么它不必继承关于类 A 的所有内容。 它可以完成 A 类所做的某些事情。

通常,多态是以不同形式出现的能力。 从技术上讲,它是重新定义派生类的方法的能力。 多态与将特定实现应用于接口或更通用的基类有关。

多态是重新定义派生类的方法的能力。

Program.cs

  1. using System;
  2. namespace Polymorphism
  3. {
  4. abstract class Shape
  5. {
  6. protected int x;
  7. protected int y;
  8. public abstract int Area();
  9. }
  10. class Rectangle : Shape
  11. {
  12. public Rectangle(int x, int y)
  13. {
  14. this.x = x;
  15. this.y = y;
  16. }
  17. public override int Area()
  18. {
  19. return this.x * this.y;
  20. }
  21. }
  22. class Square : Shape
  23. {
  24. public Square(int x)
  25. {
  26. this.x = x;
  27. }
  28. public override int Area()
  29. {
  30. return this.x * this.x;
  31. }
  32. }
  33. class Program
  34. {
  35. static void Main(string[] args)
  36. {
  37. Shape[] shapes = { new Square(5), new Rectangle(9, 4), new Square(12) };
  38. foreach (Shape shape in shapes)
  39. {
  40. Console.WriteLine(shape.Area());
  41. }
  42. }
  43. }
  44. }

在上面的程序中,我们有一个抽象的Shape类。 此类演变为两个后代类别:RectangleSquare。 两者都提供了自己的Area()方法实现。 多态为 OOP 系统带来了灵活性和可伸缩性。

  1. public override int Area()
  2. {
  3. return this.x * this.y;
  4. }
  5. ...
  6. public override int Area()
  7. {
  8. return this.x * this.x;
  9. }

RectangleSquare类具有Area()方法的自己的实现。

  1. Shape[] shapes = { new Square(5), new Rectangle(9, 4), new Square(12) };

我们创建三个形状的数组。

  1. foreach (Shape shape in shapes)
  2. {
  3. Console.WriteLine(shape.Area());
  4. }

我们遍历每个形状,并在其上调用Area()方法。 编译器为每种形状调用正确的方法。 这就是多态的本质。

C# 密封类

sealed关键字用于防止意外地从类派生。 密封类不能是抽象类。

Program.cs

  1. using System;
  2. namespace DerivedMath
  3. {
  4. sealed class Math
  5. {
  6. public static double GetPI()
  7. {
  8. return 3.141592;
  9. }
  10. }
  11. class Derived : Math
  12. {
  13. public void Say()
  14. {
  15. Console.WriteLine("Derived class");
  16. }
  17. }
  18. class Program
  19. {
  20. static void Main(string[] args)
  21. {
  22. var dm = new Derived();
  23. dm.Say();
  24. }
  25. }
  26. }

在上面的程序中,我们有一个Math基类。 该类的唯一目的是为程序员提供一些有用的方法和常量。 (出于简单起见,在我们的案例中,我们只有一种方法。)它不是从继承而创建的。 为了防止不知情的其他程序员从此类中派生,创建者创建了sealed类。 如果尝试编译该程序,则会出现以下错误:’Derived’不能从密封类’Math’派生。

C# 深层副本与浅层副本

数据复制是编程中的重要任务。 对象是 OOP 中的复合数据类型。 对象中的成员字段可以按值或按引用存储。 可以以两种方式执行复制。

浅表副本将所有值和引用复制到新实例中。 引用所指向的数据不会被复制; 仅指针被复制。 新的引用指向原始对象。 对引用成员的任何更改都会影响两个对象。

深层副本将所有值复制到新实例中。 如果成员存储为引用,则深层副本将对正在引用的数据执行深层副本。 创建一个引用对象的新副本。 并存储指向新创建对象的指针。 对这些引用对象的任何更改都不会影响该对象的其他副本。 深拷贝是完全复制的对象。

如果成员字段是值类型,则将对该字段进行逐位复制。 如果该字段是引用类型,则复制引用,但不是复制引用的对象。 因此,原始对象中的引用和克隆对象中的引用指向同一对象。 (来自 programmingcorner.blogspot.com 的明确解释)

C# 浅表复制

以下程序执行浅表复制。

Program.cs

  1. using System;
  2. namespace ShallowCopy
  3. {
  4. class Color
  5. {
  6. public int red;
  7. public int green;
  8. public int blue;
  9. public Color(int red, int green, int blue)
  10. {
  11. this.red = red;
  12. this.green = green;
  13. this.blue = blue;
  14. }
  15. }
  16. class MyObject : ICloneable
  17. {
  18. public int id;
  19. public string size;
  20. public Color col;
  21. public MyObject(int id, string size, Color col)
  22. {
  23. this.id = id;
  24. this.size = size;
  25. this.col = col;
  26. }
  27. public object Clone()
  28. {
  29. return new MyObject(this.id, this.size, this.col);
  30. }
  31. public override string ToString()
  32. {
  33. var s = String.Format("id: {0}, size: {1}, color:({2}, {3}, {4})",
  34. this.id, this.size, this.col.red, this.col.green, this.col.blue);
  35. return s;
  36. }
  37. }
  38. class Program
  39. {
  40. static void Main(string[] args)
  41. {
  42. var col = new Color(23, 42, 223);
  43. var obj1 = new MyObject(23, "small", col);
  44. var obj2 = (MyObject) obj1.Clone();
  45. obj2.id += 1;
  46. obj2.size = "big";
  47. obj2.col.red = 255;
  48. Console.WriteLine(obj1);
  49. Console.WriteLine(obj2);
  50. }
  51. }
  52. }

这是一个浅表副本的示例。 我们定义了两个自定义对象:MyObjectColorMyObject对象将引用 Color 对象。

  1. class MyObject : ICloneable

我们应该为要克隆的对象实现ICloneable接口。

  1. public object Clone()
  2. {
  3. return new MyObject(this.id, this.size, this.col);
  4. }

ICloneable接口迫使我们创建Clone()方法。 此方法返回具有复制值的新对象。

  1. var col = new Color(23, 42, 223);

我们创建Color对象的实例。

  1. var obj1 = new MyObject(23, "small", col);

创建MyObject类的实例。 Color对象的实例传递给构造器。

  1. var obj2 = (MyObject) obj1.Clone();

我们创建obj1对象的浅表副本,并将其分配给obj2变量。 Clone()方法返回Object,我们期望MyObject。 这就是我们进行显式转换的原因。

  1. obj2.id += 1;
  2. obj2.size = "big";
  3. obj2.col.red = 255;

在这里,我们修改复制对象的成员字段。 我们增加id,将size更改为"big",然后更改颜色对象的红色部分。

  1. Console.WriteLine(obj1);
  2. Console.WriteLine(obj2);

Console.WriteLine()方法调用obj2对象的ToString()方法,该方法返回对象的字符串表示形式。

  1. $ dotnet run
  2. id: 23, size: small, color:(255, 42, 223)
  3. id: 24, size: big, color:(255, 42, 223)

我们可以看到 ID 是不同的(23 对 24)。 大小不同(smallbig)。 但是,这两个实例的颜色对象的红色部分相同(255)。 更改克隆对象的成员值(idsize)不会影响原始对象。 更改引用对象(col)的成员也影响了原始对象。 换句话说,两个对象都引用内存中的同一颜色对象。

C# 深层复制

要更改此行为,我们接下来将做一个深层复制。

Program.cs

  1. using System;
  2. namespace DeepCopy
  3. {
  4. class Color : ICloneable
  5. {
  6. public int red;
  7. public int green;
  8. public int blue;
  9. public Color(int red, int green, int blue)
  10. {
  11. this.red = red;
  12. this.green = green;
  13. this.blue = blue;
  14. }
  15. public object Clone()
  16. {
  17. return new Color(this.red, this.green, this.blue);
  18. }
  19. }
  20. class MyObject : ICloneable
  21. {
  22. public int id;
  23. public string size;
  24. public Color col;
  25. public MyObject(int id, string size, Color col)
  26. {
  27. this.id = id;
  28. this.size = size;
  29. this.col = col;
  30. }
  31. public object Clone()
  32. {
  33. return new MyObject(this.id, this.size,
  34. (Color)this.col.Clone());
  35. }
  36. public override string ToString()
  37. {
  38. var s = String.Format("id: {0}, size: {1}, color:({2}, {3}, {4})",
  39. this.id, this.size, this.col.red, this.col.green, this.col.blue);
  40. return s;
  41. }
  42. }
  43. class Program
  44. {
  45. static void Main(string[] args)
  46. {
  47. var col = new Color(23, 42, 223);
  48. var obj1 = new MyObject(23, "small", col);
  49. var obj2 = (MyObject) obj1.Clone();
  50. obj2.id += 1;
  51. obj2.size = "big";
  52. obj2.col.red = 255;
  53. Console.WriteLine(obj1);
  54. Console.WriteLine(obj2);
  55. }
  56. }
  57. }

在此程序中,我们对对象执行深层复制。

  1. class Color : ICloneable

现在,Color类实现了ICloneable接口。

  1. public object Clone()
  2. {
  3. return new Color(this.red, this.green, this.blue);
  4. }

我们也为Color类提供了Clone()方法。 这有助于创建引用对象的副本。

  1. public object Clone()
  2. {
  3. return new MyObject(this.id, this.size,
  4. (Color) this.col.Clone());
  5. }

当我们克隆MyObject时,我们根据col引用类型调用Clone()方法。 这样,我们也可以获得颜色值的副本。

  1. $ dotnet run
  2. id: 23, size: small, color:(23, 42, 223)
  3. id: 24, size: big, color:(255, 42, 223)

现在,所引用的Color对象的红色部分不再相同。 原始对象保留了其先前的值(23)。

C# 异常

异常是为处理异常的发生而设计的,这些特殊情况会改变程序执行的正常流程。 引发或引发异常。

在执行应用期间,许多事情可能出错。 磁盘可能已满,我们无法保存文件。 当我们的应用尝试连接到站点时,互联网连接可能会断开。 所有这些都可能导致我们的应用崩溃。 程序员有责任处理可以预期的错误。

trycatchfinally关键字用于处理异常。

Program.cs

  1. using System;
  2. namespace DivisionByZero
  3. {
  4. class Program
  5. {
  6. static void Main(string[] args)
  7. {
  8. int x = 100;
  9. int y = 0;
  10. int z;
  11. try
  12. {
  13. z = x / y;
  14. }
  15. catch (ArithmeticException e)
  16. {
  17. Console.WriteLine("An exception occurred");
  18. Console.WriteLine(e.Message);
  19. }
  20. }
  21. }
  22. }

在上面的程序中,我们有意将数字除以零。 这会导致错误。

  1. try
  2. {
  3. z = x / y;
  4. }

容易出错的语句放置在try块中。

  1. catch (ArithmeticException e)
  2. {
  3. Console.WriteLine("An exception occurred");
  4. Console.WriteLine(e.Message);
  5. }

异常类型跟随catch关键字。 在我们的情况下,我们有一个ArithmeticException。 由于算术,转换或转换操作中的错误而引发此异常。 发生错误时,将执行catch关键字之后的语句。 发生异常时,将创建一个异常对象。 从该对象中,我们获得Message属性并将其打印到控制台。

  1. $ dotnet run
  2. An exception occurred
  3. Attempted to divide by zero.

代码示例的输出。

C# 未捕获的异常

当前上下文中任何未捕获的异常都会传播到更高的上下文,并寻找适当的catch块来处理它。 如果找不到任何合适的catch块,则 .NET 运行时的默认机制将终止整个程序的执行。

Program.cs

  1. using System;
  2. namespace UcaughtException
  3. {
  4. class Program
  5. {
  6. static void Main(string[] args)
  7. {
  8. int x = 100;
  9. int y = 0;
  10. int z = x / y;
  11. Console.WriteLine(z);
  12. }
  13. }
  14. }

在此程序中,我们除以零。 没有自定义异常处理。

  1. $ dotnet run
  2. Unhandled Exception: System.DivideByZeroException: Division by zero
  3. at UncaughtException.Main () [0x00000]

C# 编译器给出了以上错误消息。

C# IOException

发生 I/O 错误时,将抛出IOException。 在下面的示例中,我们读取文件的内容。

Program.cs

  1. using System;
  2. using System.IO;
  3. namespace ReadFile
  4. {
  5. class Program
  6. {
  7. static void Main(string[] args)
  8. {
  9. var fs = new FileStream("langs.txt", FileMode.OpenOrCreate);
  10. try
  11. {
  12. var sr = new StreamReader(fs);
  13. string line;
  14. while ((line = sr.ReadLine()) != null)
  15. {
  16. Console.WriteLine(line);
  17. }
  18. }
  19. catch (IOException e)
  20. {
  21. Console.WriteLine("IO Error");
  22. Console.WriteLine(e.Message);
  23. }
  24. finally
  25. {
  26. Console.WriteLine("Inside finally block");
  27. if (fs != null)
  28. {
  29. fs.Close();
  30. }
  31. }
  32. }
  33. }
  34. }

始终执行finally关键字之后的语句。 它通常用于清理任务,例如关闭文件或清除缓冲区。

  1. } catch (IOException e)
  2. {
  3. Console.WriteLine("IO Error");
  4. Console.WriteLine(e.Message);
  5. }

在这种情况下,我们捕获了特定的IOException异常。

  1. } finally
  2. {
  3. Console.WriteLine("Inside finally block");
  4. if (fs != null)
  5. {
  6. fs.Close();
  7. }
  8. }

这些行确保关闭文件处理器。

  1. $ cat langs.txt
  2. C#
  3. Java
  4. Python
  5. Ruby
  6. PHP
  7. JavaScript

这些是langs.txt文件的内容。

  1. $ dotnet run
  2. C#
  3. Java
  4. Python
  5. Ruby
  6. PHP
  7. JavaScript
  8. Inside finally block

这是程序的输出。

我们使用cat命令和程序输出显示langs文件的内容。

C# 多个异常

我们经常需要处理多个异常。

Program.cs

  1. using System;
  2. using System.IO;
  3. namespace MultipleExceptions
  4. {
  5. class Program
  6. {
  7. static void Main(string[] args)
  8. {
  9. int x;
  10. int y;
  11. double z;
  12. try
  13. {
  14. Console.Write("Enter first number: ");
  15. x = Convert.ToInt32(Console.ReadLine());
  16. Console.Write("Enter second number: ");
  17. y = Convert.ToInt32(Console.ReadLine());
  18. z = x / y;
  19. Console.WriteLine("Result: {0:N} / {1:N} = {2:N}", x, y, z);
  20. }
  21. catch (DivideByZeroException e)
  22. {
  23. Console.WriteLine("Cannot divide by zero");
  24. Console.WriteLine(e.Message);
  25. }
  26. catch (FormatException e)
  27. {
  28. Console.WriteLine("Wrong format of number.");
  29. Console.WriteLine(e.Message);
  30. }
  31. }
  32. }
  33. }

在此示例中,我们捕获了各种异常。 请注意,更具体的异常应先于一般的异常。 我们从控制台读取两个数字,并检查零除错误和数字格式错误。

  1. $ dotnet run
  2. Enter first number: we
  3. Wrong format of number.
  4. Input string was not in a correct format.

运行示例,我们得到了这个结果。

C# 自定义异常

定制异常是从System.Exception类派生的用户定义的异常类。

Program.cs

  1. using System;
  2. namespace CustomException
  3. {
  4. class BigValueException : Exception
  5. {
  6. public BigValueException(string msg) : base(msg) { }
  7. }
  8. class Program
  9. {
  10. static void Main(string[] args)
  11. {
  12. int x = 340004;
  13. const int LIMIT = 333;
  14. try
  15. {
  16. if (x > LIMIT)
  17. {
  18. throw new BigValueException("Exceeded the maximum value");
  19. }
  20. }
  21. catch (BigValueException e)
  22. {
  23. Console.WriteLine(e.Message);
  24. }
  25. }
  26. }
  27. }

我们假定存在无法处理大量数字的情况。

  1. class BigValueException : Exception

我们有一个BigValueException类。 该类派生自内置的Exception类。

  1. const int LIMIT = 333;

大于此常数的数字在我们的程序中被视为big

  1. public BigValueException(string msg) : base(msg) {}

在构造器内部,我们称为父级的构造器。 我们将消息传递给父级。

  1. if (x > LIMIT)
  2. {
  3. throw new BigValueException("Exceeded the maximum value");
  4. }

如果该值大于限制,则抛出自定义异常。 我们给异常消息Exceeded the maximum value

  1. } catch (BigValueException e)
  2. {
  3. Console.WriteLine(e.Message);
  4. }

我们捕获到异常并将其消息打印到控制台。

  1. $ dotnet run
  2. Exceeded the maximum value

This is the output of the program.

在 C# 教程的这一部分中,我们继续讨论 C# 中的面向对象编程。