# C# 面向对象编程 II

> 原文： [https://zetcode.com/lang/csharp/oopii/](https://zetcode.com/lang/csharp/oopii/)

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

## C# 接口

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

接口是：

*   蜜蜂
*   合约

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

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

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

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

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

## C# 简单接口

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

`Program.cs`

```cs
using System;

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# 程序。

```cs
interface IInfo
{
    void DoInform();   
}

```

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

```cs
class Some : IInfo

```

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

```cs
public void DoInform()
{
    Console.WriteLine("This is Some Class");
}

```

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

## C# 多个接口

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

`Program.cs`

```cs
using System;

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`类，它从三个接口继承。

```cs
class CellPhone : Device, Volume, Pluggable

```

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

```cs
$ dotnet run
Switching on
Volume up
Plugging In

```

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

## C# 多接口继承

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

`Program.cs`

```cs
using System;

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();
        }
    }
}

```

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

```cs
interface ILog : IInfo, IVersion

```

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

```cs
public void DoInform() 
{
    Console.WriteLine("This is DBConnect class");
}

```

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

```cs
$ dotnet run
This is DBConnect class
Version 1.02
Logging
Connecting to the database

```

这是输出。

## C# 多态性

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

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

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

`Program.cs`

```cs
using System;

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());
            }
        }
    }
}

```

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

```cs
public override int Area()
{
    return this.x * this.y;
}
...
public override int Area()
{
    return this.x * this.x;
}

```

`Rectangle`和`Square`类具有`Area()`方法的自己的实现。

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

```

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

```cs
foreach (Shape shape in shapes)
{
    Console.WriteLine(shape.Area());
}

```

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

## C# 密封类

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

`Program.cs`

```cs
using System;

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();
        }
    }
}

```

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

## C# 深层副本与浅层副本

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

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

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

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

### C# 浅表复制

以下程序执行浅表复制。

`Program.cs`

```cs
using System;

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`对象将引用 Color 对象。

```cs
class MyObject : ICloneable

```

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

```cs
public object Clone()
{
    return new MyObject(this.id, this.size, this.col);
}

```

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

```cs
var col = new Color(23, 42, 223);  

```

我们创建`Color`对象的实例。

```cs
var obj1 = new MyObject(23, "small", col);

```

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

```cs
var obj2 = (MyObject) obj1.Clone();

```

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

```cs
obj2.id += 1;
obj2.size = "big";         
obj2.col.red = 255;

```

在这里，我们修改复制对象的成员字段。 我们增加`id`，将`size`更改为“大”，然后更改颜色对象的红色部分。

```cs
Console.WriteLine(obj1);
Console.WriteLine(obj2);

```

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

```cs
$ dotnet run
id: 23, size: small, color:(255, 42, 223)
id: 24, size: big, color:(255, 42, 223)

```

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

### C# 深层复制

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

`Program.cs`

```cs
using System;

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);
        }
    }
}

```

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

```cs
class Color : ICloneable

```

现在，`Color`类实现了`ICloneable`接口。

```cs
public object Clone()
{
    return new Color(this.red, this.green, this.blue);
}

```

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

```cs
public object Clone()
{
    return new MyObject(this.id, this.size, 
        (Color) this.col.Clone());
}

```

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

```cs
$ dotnet run
id: 23, size: small, color:(23, 42, 223)
id: 24, size: big, color:(255, 42, 223)

```

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

## C# 异常

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

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

`try`，`catch`和`finally`关键字用于处理异常。

`Program.cs`

```cs
using System;

namespace DivisionByZero
{
    class Program
    {
        static void Main(string[] args)
        {
            int x = 100;
            int y = 0;
            int z;

            try
            {
                z = x / y;
            } 
             catch (ArithmeticException e)
            {
                Console.WriteLine("An exception occurred");
                Console.WriteLine(e.Message);
            }
        }
    }
}

```

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

```cs
try
{
    z = x / y;
}

```

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

```cs
 catch (ArithmeticException e)
{
    Console.WriteLine("An exception occurred");
    Console.WriteLine(e.Message);
}

```

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

```cs
$ dotnet run
An exception occurred
Attempted to divide by zero.

```

代码示例的输出。

### C# 未捕获的异常

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

`Program.cs`

```cs
using System;

namespace UcaughtException
{
    class Program
    {
        static void Main(string[] args)
        {
            int x = 100;
            int y = 0;

            int z = x / y;

            Console.WriteLine(z);
        }
    }
}

```

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

```cs
$ dotnet run

Unhandled Exception: System.DivideByZeroException: Division by zero
  at UncaughtException.Main () [0x00000]

```

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

### C# `IOException`

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

`Program.cs`

```cs
using System;
using System.IO;

namespace ReadFile
{
    class Program
    {
        static void Main(string[] args)
        {
            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`关键字之后的语句。 它通常用于清理任务，例如关闭文件或清除缓冲区。

```cs
} catch (IOException e)
{
    Console.WriteLine("IO Error");
    Console.WriteLine(e.Message);
} 

```

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

```cs
} finally
{
    Console.WriteLine("Inside finally block");

    if (fs != null)  
    {
        fs.Close();
    }
}

```

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

```cs
$ cat langs.txt
C#
Java
Python
Ruby
PHP
JavaScript

```

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

```cs
$ dotnet run
C#
Java
Python
Ruby
PHP
JavaScript
Inside finally block

```

这是程序的输出。

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

### C# 多个异常

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

`Program.cs`

```cs
using System;
using System.IO;

namespace MultipleExceptions
{
    class Program
    {
        static void Main(string[] args)
        {
            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);
            }
        }
    }
}

```

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

```cs
$ dotnet run
Enter first number: we
Wrong format of number.
Input string was not in a correct format.

```

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

### C# 自定义异常

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

`Program.cs`

```cs
using System;

namespace CustomException
{
    class BigValueException : Exception
    {
        public BigValueException(string msg) : base(msg) { }
    }

    class Program
    {
        static void Main(string[] args)
        {
            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);
            }
        }
    }
}

```

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

```cs
class BigValueException : Exception

```

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

```cs
const int LIMIT = 333;

```

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

```cs
public BigValueException(string msg) : base(msg) {}

```

在构造函数内部，我们称为父级的构造函数。 我们将消息传递给父母。

```cs
if (x > LIMIT)
{
    throw new BigValueException("Exceeded the maximum value");
}

```

如果该值大于限制，则抛出自定义异常。 我们给异常消息“超出最大值”。

```cs
} catch (BigValueException e)
{
    Console.WriteLine(e.Message);
}

```

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

```cs
$ dotnet run
Exceeded the maximum value

```

This is the output of the program.

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