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

发布于:2022-12-20 ⋅ 阅读:(404) ⋅ 点赞:(0)

一、提要

        在 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
本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到