• C#教程10:面向对象 II(接口继承)


    一、提要

            在 C# 教程的这一章中,我们继续描述 OOP。我们涵盖了接口、多态性、深浅复制、密封类和异常。

    二、C# 接口

            遥控器是观众和电视之间的接口。它是该电子设备的接口。外交礼仪指导外交领域的所有活动。道路规则是驾车者、骑自行车者和行人必须遵守的规则。

            编程中的接口类似于前面的示例。接口就是:

    • APIs
    • Contracts

            对象通过它们公开的方法与外部世界交互。实际的实现对程序员来说并不重要,或者它也可能是秘密的。一家公司可能会出售一个库,但它不想透露实际的实现。程序员可能会在 GUI 工具包的窗口上调用最大化方法,但对如何实现此方法一无所知。从这个角度来看,接口是对象与外部世界交互的方式,而不会过多地暴露其内部工作原理。

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

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

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

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

    三、C#示例接口( interface)

            下面的程序使用一个简单的界面。

    Program.cs

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

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

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

            这是一个接口 IInfo。它具有 DoInform 方法签名。

    class Some : IInfo
    

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

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

            该类提供了doInfo方法的实现。

    四、多接口( multiple interfaces)

    下一个示例展示如何实现多重接口。

    Program.cs

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

            我们实现了 CellPhone 类,它继承了三个接口.

             class CellPhone : Device, Volume, Pluggable
    

            该类实现了所有三个接口,它们由逗号分隔。CellPhone类必须实现所有三个接口的所有方法签名。

    $ dotnet run
    Switching on
    Volume up
    Plugging In
    

    五、多重接口继承

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

    Program.cs

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

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

            interface ILog : IInfo, IVersion
    

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

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

            DBConnect类实现doInfo方法。该方法由类实现的ILog接口继承。

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

    六、 多态性( polymorphism)

            多态性是以不同方式为不同数据输入使用运算符或函数的过程。实际上,多态性意味着,如果类B继承自类A,则它不必继承关于类A的一切;它可以做一些A类不同的事情。

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

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

    Program.cs

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

            在上面的程序中,我们有一个抽象的形状类。该类变形为两个子类:矩形和正方形。两者都提供了各自的面积方法实现。多态性为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. }

            矩形和正方形类有各自的面积方法实现。

            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. }

            我们遍历每个形状并调用其上的面积方法。编译器为每个形状调用正确的方法。这就是多态性的本质。

    七、封装类(sealed classes)

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

    Program.cs

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

            在上面的程序中,我们有一个基础数学课。这个类的唯一目的是为程序员提供一些有用的方法和常量。(在我们的例子中,为了简单起见,我们只有一个方法。)它不是为了继承而创建的。

            为了防止未知情的其他程序员从该类派生,创建者将该类密封。如果您尝试编译此程序,会出现以下错误:“派生”无法从密封类型“数学”派生。

    八、深拷贝和浅拷贝( deep copy vs shallow copy)

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

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

            深度副本将所有值复制到新实例中。对于存储为引用的成员,深度副本将执行被引用数据的深度副本。将创建引用对象的新副本。并且存储指向新创建的对象的指针。对这些引用对象的任何更改都不会影响对象的其他副本。深度副本是完全复制的对象。

            如果成员字段是值类型,则执行字段的逐位复制。如果字段是引用类型,则复制引用,但不复制引用对象;因此,原始对象中的引用和克隆中的引用指向同一对象。

    8.1 浅拷贝

    下述代码示范浅拷贝。

    Program.cs

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

            这是一个浅拷贝的示例。我们定义了两个自定义对象: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。这就是我们进行显式转换的原因。

    1. obj2.id += 1;
    2. obj2.size = "big";
    3. 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

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

            在这个程序中,我们对对象执行深度复制

            class Color : ICloneable
    

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

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

            我们也为颜色类提供了一个克隆方法。这有助于创建引用对象的副本。

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

            克隆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
  • 相关阅读:
    Python零基础入门篇 · 21】:构造函数、类属性和实例属性的访问
    Web前端:编程讨论——React与Angular对比
    python字符串
    CNN特征可视化相关论文
    二次型和矩阵正定的意义
    2023华为杯研究生数学建模竞赛CDEF题思路+模型代码
    Python基础内容训练4(常用的数据类型-----元组)
    适合自学的网络安全基础技能“蓝宝书”:《CTF那些事儿》
    IPC通信
    2022杭电多校联赛第五场 题解
  • 原文地址:https://blog.csdn.net/gongdiwudu/article/details/123445836