C#中的面向对象概念

原著:Tushar Kant Agrawal 12/24/2003
原文http://www.c-sharpcorner.com/Code/2003/Dec/OOPS In CSharp 1.0.asp
翻译:lover_P
出处http://www.cstc.net.cn/docs/docs.php?id=254


    在这篇文章中我们将要讨论一些面向对象在C#中的具体实践的关键概念。我们将要讨论一下面向对象的基础包括接口(Interface)、访问修饰符(Access Modifier)、继承(Inheritance)、多态(Polymorphism)等等。

面向对象的关键概念
    抽象(Abstraction)
    封装(Encapsulation)
    多态(Polymorphism)
    继承(Inheritance)

[内容]

 

抽象

    抽象是一种将一个对象归纳为一个数据类型的能力,这个数据类型具有特定的一组特征(Characteristic)和能够执行的一组行为(Action)。

    面向对象的(Object-oriented)语言通过类(Class)来提供抽象。类为一个对象(Object)的类型定义了属性(Porperty)和方法(Method)。

    例如:

  • 你可以为狗狗建立一个抽象,它具有一些特征,如颜色、身高[译注:狗狗也要记录身高?]和重量;还有一些行为,如跑和咬。我们称这些特征为属性,称这些行为为方法。
  • 一个记录集(Recoredset)对象是对一组数据的集合的抽象表示。

是对象的蓝图。
对象是类的实例(Instance)。

C#中类的例子:

public class Draw {
    // 类代码
}

对象引用

    当我们在操作一个对象时,我们需要使用对该对象的一个引用(Reference)。相反,当我们操作简单数据类型如整型时,我们操作的是实际的值而不是引用。

    当我们使用new关键字建立一个新的对象时,我们便将该对象的一个引用存贮在了一个变量中。看下面的例子:

Draw MyDraw = new Draw();

    这段代码建立了Draw类的一个新的实例。我们通过MyDraw变量来获得对这个新对象的访问。这个变量保存了对这个对象的引用。

    现在我们还有另外一个变量,它具有对同一个对象的引用。我们可以随意使用这两个变量,因为他们都引用了同一个对象。我们需要记得的是我们的这个变量并不是对象本身,它只是对对象本身的一个引用或指针。

    早绑定(Early binding)是指我们的代码通过直接调用对象的方法来直接与对象进行交互。由于编译器事先知道对象的数据类型,它可以直接编译出调用对象方法的代码。早绑定还允许IDE通过使用智能感知(IntelliSense)来帮助我们进行开发工作;它还能够使编译器确保我们所调用的方法确实存在,以及我们确实提供了恰当的参数值。

    迟绑定(Late Binding)是指我们的代码在运行时动态地与对象进行交互。这提供了很大的灵活性,因为我们的代码不必知道它所交互的对象的具体类型,只要这个对象提供了我们需要调用的方法就可以了。由于IDE和编译器无法知道对象的具体类型,也就无法进行智能感知和编译期间语法检查;但是,相较而言,我们却得到了空前的灵活性。

    如果我们通过在我们的代码模块的顶部指定“Option Strict On”[译注:指的是Visual Basic .NET]来打开严格类型检查,则IDE和编译器将强制进行早绑定。默认的情况下,严格类型检查被设置为关闭状态,所以我们可以在我们的代码中使用迟绑定。

访问修饰符

    访问修饰符是一组关键字,用于指定在一个类型中声明的成员的可访问性。

    公有(Public)成员对于任何人都是可见的。我们可以在类内部的和类的子孙的代码中通过使用类的实例来访问一个公有的成员。

    私有(Private)成员是隐藏的,只有对类本身是可用的。所有使用了一个类的实例的代码都不能直接访问一个私有成员,这个类的子类也不允许。

    受保护(Protected)成员和私有成员类似,只能由包含它的类访问。然而,受保护成员可以由一个类的子类所使用。如果类中的一个成员可能被该类的子类访问,它应该声明未受保护的。

    内部/友元(Internal/Friend)成员对整个应用程序是公有的,但对于其他的外部应用程序是私有的。当我们希望其他应用程序能够使用当前应用程序中的包含的一个类,但又希望这个类为当前应用程序保留一定的功能时,就要用到内部/友元成员。在C#中我们使用internal,而在Visual Basic .NET中我们使用Friend

    受保护内部(Protected Internal)成员只能由包含了基类的应用程序中的从该基类派生的子类所访问。当你希望一个类中的成员只能由其子类访问,并且拒绝其他应用程序访问该类的这个成员的时候,你就要将其声明未受保护内部成员。

对象的成分

    我们使用接口来获得对对象的数据和行为的访问。对象的数据和行为包含在对象内部,因此,一个客户应用程序可以将对象视为黑盒,只有通过它的接口才能获得可访问性。这是面向对象的关键概念,称为封装。这意味着任何使用这个对象的应用程序不能直接访问它的行为和数据——必须使用对象的接口。

对象有三个主要部分:

  1. 接口(Interface)
  2. 实现(Implementation)或行为(Behavior)
  3. 成员(Member)或实例变量(Instance variable)

接口

    接口定义了一组方法(Method,子程序或函数例程[译注:指Visual Basic .NET中的Sub和Function])、属性(Property)、事件(Event)和域(Field,变量或特性),这些都被声明为公有。

实现或行为

    一个方法之内的代码称为实现。有的时候由于这些代码可以使对象作一些有用的工作,而称之为行为。

    尽管我们改变了实现,客户程序仍然可以使用我们的对象——只要我们没有改变接口。只要我们的方法的名字和它的参数列表以及返回值类型没有改变,我们可以随意地改变它的实现。
因此方法签名(Method Signature)取决于:

  • 方法名称
  • 参数的数据类型
  • 参数是按值传递还是按引用传递[译注:Visual Basic .NET中的ByVal或ByRef]
  • [译注:还有参数的出现顺序]

    很重要的需要我们紧记的是,封装只是一个语法工具——它允许我们现有的代码可以无需修改而继续运行。然而,这不是语义的——这意味着尽管我们现有的代码可以运行,但它不一定继续作我们希望它做的工作。

成员或实例变量

    一个对象的第三个关键部分是数据(Data),或状态(State)。一个类的每一个实例都具有绝对相同的接口和实现——唯一可以不同的就是特定对象中所包含的数据。

    成员变量正是为此而声明,它对于我们的类总的所有的代码都是可用的。典型的成员变量通常被声明为私有的——只有对我们的类本身的代码有效。它们有时也被称作实例变量或特性。.NET Framework也称它们为域。

    我们不要将实例变量与属性相混淆。属性是一种特殊的方法,用来获取或设置数据;而实例变量是类中的一个变量,用来保存被属性所暴露的数据。

    接口看起来像一个类,但没有实现。其中只包含事件、索引器(Indexer)、方法和/或属性的定义。接口中只提供定义的原因是因为它们必须被类和结构继承,这些类和结构必须对接口中定义的每个成员提供实现。那么,没有实现任何功能的接口有什么好处呢?他们可以装配出一个“即插即用”的架构,其中的所有组件可以随意替换!由于所有可替换的组件都实现了相同的接口,因此可以不用任何扩展程序就可以使用它们。接口强制每个组件暴露用于特定途径的公有成员。

    因为接口必须有继承的类和结构所定义,因此它们定义了一个契约。对于实例,如果类foo是从接口IDisposable继承而来的,这将形成一个条款,确保它具有Dispose()方法,这是IDisposable接口中唯一的一个成员。所有希望使用foo类的代码都会检查foo类是否继承自IDisposable接口。如果答案为真,代码就知道了它可以调用foo.Dispose()

    定义一个接口:MyInterface.cs

interface ImyInterface {
    void MethodToImplement();
}

    上面的代码定义了一个名为IMyInterface的接口。一个通用的命名约定是在所有的接口名字前面添加前缀字母“I”,但这不是强制的。这个接口有一个单独的名为MethodToImplement()的方法。接口可以拥有任何类型的、带有不同参数和不同返回值类型 的方法。注意这个方法没有实现(花括号“{}”之间的指令),而是直接以分号“;”结束。这是因为接口仅指定方法的签名而必须由继承它的类或结构去实现。

    接口中的所有方法都默认为公有的,而且对于任何方法或接口[译注:指的是接口中声明的嵌套接口]不允许出现访问修饰符(如privatepublic)。

    使用一个接口:InterfaceImplementer.cs

class InterfaceImplementer : IMyInterface {
    public void MethodToImplement() {
        Console.WriteLine(“MethodToImplement() called.”);
    }
}

    上面代码中的InterfaceImplement类实现了IMyInterface接口。指定一个类继承了一个接口和指定它继承了一个类是一样的,都用到了下面的语法:

class InterfaceImplementer : IMyInterface

    注意,既然这个类继承了IMyInterface接口,它就必须实现所有的成员。当实现接口中的方法时,所有这些方法都必须且仅能声明为公有的。这个类通过实现MethodToImplement()方法来完成这些。注意这个方法和接口中定义的方法具有完全相同的签名、参数和方法名称。任何的不一致都会产生编译错误。接口同样还可以继承自其它接口。下面的代码显示了如何实现继承的接口。

    接口继承:InterfaceInheritance.cs

using System;

interface IParentInterface {
    void ParentInterfaceMethod();
}

interface IMyInterface : IParentInterface {
    void MythodToImplement();
}

class InterfaceIplementer : IMyInterface {
    public void MethodToImplement() {
        Console.WriteLine(“MethodToImplement() called.”);
    }

    public void ParentInterfaceMethod() {
        Console.WriteLine(“ParentInterfaceMethod() called.”);
    }
}

    上面的代码中有两个接口:IMyInterface接口和它所继承的IParentInterface接口。当一个接口继承了另一个,实现它的类或结构必须实现整个接口继承链中所有的接口成员。由于上面代码中的InterfaceImplementer类继承了IMyInterface,它同时也继承了IParentInterface。因此,InterfaceImplementer类必须实现IMyInterface接口中指定的MethodToImplement()方法和IParentInterface接口中指定的ParentInterfaceMethod()方法。

    总的来说,你可以实现一个接口并在类中使用它。接口也可以被另一个接口继承。任何继承了一个接口的类或结构都必须实现接口继承链中所有接口所定义的成员。

继承

    继承是指一个类——称为子类[译注:亦称派生类],可以基于另一个类——称为基类[译注:亦称父类、超类]。继承提供了一种建立对象层次的机制。

    继承使得你能够在你自己的类中使用另外一个类的接口和代码。

    标准的基类既可以是第一次声明的,也可以是[从其它类]继承的。派生类(Derived class)可以继承基类中具有受保护或更高访问性的成员。除了父类所提供的功能之外,派生类还提供了更多专门的功能。在派生类中继承基类成员不是强制的。

访问关键字

    base->访问基类的成员。
    this->引用调用一个方法的当前对象。

    base关键字用于在一个派生类中访问基类的成员:在基类上调用一个以被其它方法重写了的方法、在建立派生类的一个实例的时候指定哪一个基类构造器应该被调用。对基类的访问只允许出现在构造器、实例方法或实例属性访问器中。

    在下面的例子中,基类Person和派生类Employee都有一个名为Getinfo()的方法。通过使用base关键字,可以从派生类中调用基类的Getinfo()方法。

// 访问基类成员
using System;

public class Person {
    protected string ssn = “444-55-6666”;
    protected string name = “John L. Malgraine”;

    public virtual void GetInfo() {
        Console.WriteLine(“Name: {0}”, name);
        Console.WriteLine(“SSN: {0}”, ssn);
    }
}

class Employee : Person {
    public string id = “ABC567EFG”;

    public override void GetInfo() {
        // 调用基类中的GetInfo()方法:
        base.GetInfo();
        Console.WriteLine(“Employee ID: {0}”, id);
    }
}

class TestClass {
    public static void Main() {
        Employee E = new Employee();
        E.GetInfo();
    }
}

输出为:

Name: John L. Malgraine
SSN: 444-55-6666
Employee ID: ABC567EFG

    基类构造器也可以在派生类中调用。要调用一个基类构造器,可以使用base()引用基类构造器。当需要恰当地初始化一个积累的时候这就非常必要了。

    下面的例子展示了一个带有address参数的派生类构造器:

abstract public class Contact {
    private string address;

    public Contact(string b_address) {
        this.address = b_address;
    }
}

public class Customer : Contact {
    public Customer(string c_address) : base(c_address) {
    }
}

    而在这段代码中,Customer类并没有address,因此它通过在其声明添加一个冒号和带有参数的base关键字将参数传递给它的基类构造器。这会调用Contact的带有address参数的构造器,并将Contactaddress域初始化。

    下面是另外一个例子,也展示了当建立派生类实例时是如何调用基类构造器的:

using System;

public class MyBase {
    int num;

    public MyBase() {
        Console.WriteLine(“In MyBase()”);
    }

    public MyBase(int i) {
        num = i;
        Console.WriteLine(“in MyBase(int i)”);
    }
}

public class MyDerived : MyBase {
    static int i = 32;

    // 该构造器将调用MyBase.MyBase()
    public MyDerived(int ii) : base() {
    }

    // 该构造器将调用MyBase.MyBase(int i)
    public MyDerived() : base(i) {
    }

    public static void Main() {
        MyDerived md = new MyDerived(); // 调用public MyDerived() : base(i)
        // 并将i = 32传递给基类
        MyDerived md1 = new MyDerived(1); // 调用public MyDerived() : base()
    }
}

输出为:

in MyBase(int i)
in MyBase()

    下面的例子不会通过编译。它详细地说明了一个类定义如果不包括默认构造器后果:

abstract public class Contact {
    private string address;

    public Contact(string address) {
        this.address = address;
    }
}

public class Customer : Contact {
    public Customer(string address) {
    }
}

    在这个例子中,Customer的构造器没有调用基类构造器。这很明显是个Bug,因为address域从未被初始化。

    当一个类没有一个显式的构造器时,系统会为它指派一个默认构造器。默认构造器自动地调用一个默认的或无参的基类构造器。下面的例子是上述例子中将会出现的一个自动生成的默认构造器:

public Customer() : Contact() {
}

    当一个类没有声明任何构造器时,这个例子中的代码会自动地生成。当没有定义派生类构造器时,默认的基类构造器会被隐式地调用。一旦定义了一个派生类构造器,无论其是否带有参数,都不会自动定义上例中出现的默认构造器。

调用基类成员

    如果基类的某些成员具有受保护或更高的可访问性,则派生类可以访问这些成员。这只需简单地在恰当的上下文环境中使用成员的名字,就好像这个成员是派生类自己的一样。下面是一个例子:

abstract public class Contact {
    private string address;
    private string city;
    private string state:
    private string zip:

    public string FullAddress() {
        string fullAddress = address + ‘\n’ + city + ‘,’ + state + ‘ ’ + zip;
        return fullAddress;
    }
}

public class Customer : Contact {
    public string GenerateReport() {
        string fullAddress = FullAddress();
        // 其它操作
        return fullAddress;
    }
}

    在上面的例子中,Customer类的GenerateReport()方法调用了其基类Contact中的FullAddress()方法。所有类对其自身的成员都具有完全的访问,而无须限定词。限定词(Qualification)由用圆点分开的类名字和其成员名字组成——如MyObject.SomeMethod()。这个例子说明派生类可以和访问其自身成员一样访问基类成员。

关于继承的更多提示

    不能将一个静态成员标记为重写(override)、虚拟(virtual)或抽象(abstract)的。因此,下面的一行语句是错误的:

public static virtual void GetSSN()

    你不能在派生类中使用base关键字来调用父类的静态方法。

    如果上面的例子中我们声明了下面静态方法:

public class Person {
    protected string ssn = “444-55-6666”;
    protected string name = “John L. Malgraine”;

    public static void GetInfo() {
        // 实现
    }
}

    现在你就不能使用base.GetInfo()来调用这个方法了,而必须用Person.GetInfo()来调用。在静态成员中我们只能访问静态域、静态方法等。

    下面的例子会出错,因为在GetInfo()中我们不能访问name,因为name是非静态的。

pulic class Person {
    protected string ssn = “444-55-6666”;
    protected string name = “John L. Malgraine”;

    public static void GetInfo() {
        Console.WriteLine(“Name: {0}”, name);
        Console.WriteLine(“SSN: {0}”, ssn);
    }
}

    虚拟的或抽象的成员不能是私有的。

    如果你没有在派生类中重写基类中的虚拟方法,你就不能在派生类中使用base关键字来调用基类方法。同时如果你建立了派生类的实例,你只能调用派生类的方法;如果你要访问基类中的方法,只有建立基类的实例。

    当你在派生了中重写基类中的方法时,你不能降低方法的访问级别;反之却可以。也就是说在派生类中你可以将基类中的受保护方法标记为公有的[译注:在.NET Framework 1.1和C#编译器版本7.10.3052.4中是不允许改变重写方法的访问修饰符的]。

    “this”关键字代表:

    调用一个方法的当前实例。静态成员函数中没有this指针。this关键字只能用于构造器、实例方法和实例属性访问器中。

    下面是this的一般用法:

  • 用来限定具有相同名字的成员,例如:

public Employee(string name, string alias) {
    this.name = name;
    this.alias = alias;
}

    上面的例子中,this.name代表类中的私有变量name。如果我们写name = name,这表示的是构造器的参数name而不是类中的私有变量name。这种情况下私有变量name是不会被初始化的。

  • 用来将该对象传递给其它方法,例如:

CalcTax(this);

  • 用于索引器,例如:

public int this[int param] {
    get {
        return array[param];
    }

    set {
        array[param] = value;
    }
}

    在静态方法、静态属性访问器和域声明中的变量初始化器中使用this是错误的。

    下面的例子使用了this来限制同名的类成员namealias。同时还将一个对象传递给另一个类中的方法。

// keywords_this.cs
// this示例
using System;

public class Employee {
    public string name;
    public string alias;
    public decimal salary = 3000.00m;

    // 构造器:
    public Employee(string name, string alias) {
        // 使用this来限定name和alias域:
        this.name = name;
        this.alias = alias;
    }

    // 打印方法:
    public void printEmployee() {
        Console.WriteLine("Name: {0}\nAlias: {1}", name, alias);
        // 通过使用this将当前对象传递给CalTax()方法:
        Console.WriteLine("Taxes: {0:C}", Tax.CalcTax(this));
    }
}

public class Tax {
    public static decimal CalcTax(Employee E) {
        return (0.08m * (E.salary));
    }
}

public class MainClass {
    public static void Main() {
        // 建立对象:
        Employee E1 = new Employee ("John M. Trainer", "jtrainer");

        // 显示结果:
        E1.printEmployee();
    }
}

输出为:

Name: John M. Trainer
Alias: jtrainer
Taxes: $240.00

抽象类

    抽象类是一种特殊的基类。除了通常的类成员,它们还带有抽象类成员。抽象类成员是指没有实现而只有声明的方法和属性。所有直接从抽象类派生的类都必须实现所有这些抽象方法和属性。

    抽象方法不能实例化。这样做[译注:指实例化一个抽象类]是不合逻辑的,因为那些抽象成员没有实现。那么,不能实例化一个类有什么好处呢?很多!抽象类稳坐类继承树的顶端。它们确定了类的结构和代码的意图。用它们可以得到更易搭建的框架。这是可能的,因为抽象类具有这个框架中所有基类的一般信息和行为。看看下面的例子:

abstract public class Contact { // 抽象类Contact
    protected string name;

    public Contact() {
        // 语句
    }

    public abstract void generateReport();

    abstract public string Name {
        get;
        set;
    }
}

    Contact是一个抽象类。Contact有两个抽象成员,其中有一个是抽象方法,名为generateReport()。这个方法使用了abstract修饰符进行声明,这个声明没有实现(没有花括号)并以分号结束。属性Name也被声明为抽象的。属性访问器也是以分号结束。

public class Customer : Contact { // Customer继承自抽象类Contact
    string gender;
    decimal income;
    int nuberOfVisits;

    public Customer() {
        // 语句
    }

    public override void generateReport() {
        // 产生一份独特的报告
    }

    public override string Name {
        get {
            numberOfVisits++;
            return name;
        }

        set {
            name = value;
            nuberOfVisits = 0;
        }
    }
}

public class SiteOwner : Contact {
    int siteHits;
    string mySite;

    public SiteOwner() {
        // 语句
    }

    public override void generateReport() {
        // 产生一份独特的报告
    }

    public override string Name {
        get {
            siteHits++;
            return name;
        }

        set {
            name = value;
            siteHits = 0;
        }
    }
}

    抽象基类有两个派生类——CustomerSiteOwner。这些派生类都实现了基类Contact中的抽象成员。每个派生类中的generateReport()方法声明中都有一个override修饰符。同样,CustomerSiteOwner中的Name属性的声明也都带有override修饰符。当重写方法时,C#有意地要求显式的声明。这种方法可以跳代码的安全性,因为它可以防止意外的方法重写,这在其他语言中确实发生过。省略override修饰符是错误的。同样,添加new修饰符也是错误的。抽象方法必须被重写,不能隐藏。因此既不能使用new修饰符,也不能没有修饰符。

    所有抽象类中最出名的就是Object类[译注:.NET Framework中的Object类并不是抽象类]。它可以写作objectObject[译注:object是C#中的关键字,用于声明Object类的一个对象;Object是指.NET Framework类库中的System.Object类],但它们都是同一个类。Object类是C#中所有其他类的基类。它同时也是没有指定基类时的默认基类。下面的这些类声明产生同样的结果:

abstract public class Contact : Object {
    // 类成员
}

abstract public class Contact {
    // 类成员
}

    如果没有声明基类,Object会隐式地成为基类。除了将C#类框架中的所有类联系在一起,Object类还提供了一些内建的功能,这些需要派生类来实现。

接口和抽象类之间的区别

    接口和抽象类关系很紧密,它们都具有对成员的抽象。

    对于一个抽象类,至少一个方法是抽象方法既可,这意味着它也可以具有具体方法[译注:Concrete Method,这只是相对于抽象方法而言,面向对象中并没有这个概念]。

    对于一个接口,所有的方法必须都是抽象的。

    实现了一个接口的类必须为接口中的所有方法提供具体的实现,否则只能声明为抽象类。

    在C#中,多重继承(Multiple Inheritance)只能通过实现多个接口得到。抽象类只能单继承[译注:C#中的单继承是指所有类作为基类的时候都只能是派生类声明中唯一的基类,而不仅仅是抽象类]。

    接口定义的是一个契约,其中只能包含四种实体,即方法、属性、事件和索引器。因此接口不能包含常数(Constant)、域、操作符、构造器、析构器、静态构造器或类型[译注:指嵌套的类型]。

    同时,一个接口还不能包含任何类型的静态成员。修饰符abstractpublicprotectedinternalprivatevirtualoverride都是不允许出现的,因为它们在这种环境中是没有意义的。

    类中实现的接口成员必须具有公有的可访问性。

重写概述

    派生类可以通过override关键字来重写基类中的虚拟方法。这样做必须遵守下面的约束:

  • 关键字override用于子类方法的定义,说明这个方法将要重写基类中的虚拟方法。
  • 返回值类型必须与基类中的虚拟方法一致。
  • 方法的名字必须相同。
  • 参数列表中的参数顺序、数量和类型必须一致。

    重写的方法的可访问性不能比基类中的虚拟方法具有更多的限制。可访问性应具有相同或更少的限制[译注:实际上,在C#中重写的方法的可访问性必须与基类中的虚拟方法一致]。

    子类中重写的虚拟方法可以通过sealed关键字声明为封闭(Sealed)的,以防止在以后的派生类中改变虚拟方法的实现。

隐藏基类成员

    有的时候派生了的成员和基类中相应的成员具有相同的名字。这时,我们称派生类需要“隐藏(Hiding)”基类成员。

    当发生隐藏时,派生类的成员将取代基类成员的功能。派生类的使用者将无法看到被隐藏的成员,他们只能看到派生类的成员。下面的例子显示了如何隐藏一个基类成员。

abstract public class Contact {
    private string address;
    private string city;
    private string state;
    private string zip:

    public string FullAddress() {
        string fullAddress = address + ‘\n’ + city + ‘,’ + state + ‘ ’ + zip;
        return fullAddress;
    }
}

public class SiteOwner : Contect {
    public string FullAddress() {
        string fullAddress;

        // 建立一个地址...

        return fullAddress;
    }
}

    在这个例子中,SiteOwner和他的基类——Contact——都有一个名为FullAddress()的方法。StieOwner类中的FullAddress()方法隐藏了Contact类中的FullAddress()方法。这意味着当调用一个SiteOwner类的实例的FullAddress()时,调用的是SiteOwner类的FullAddress()方法,而不是Contact类的FullAddress()方法。

    尽管基类中的成员可以被隐藏,派生类还是可以通过base关键字来访问它。有的时候这样做是值得的。这对于既要利用基类的功能又要添加派生类的代码是很有用的。下面的例子展示了如何在派生类中引用基类中(被隐藏的)成员。

abstract public class Contact {
    private string address;
    private string city;
    private string state;
    private string zip;

    public string FullAddress() {
        string fullAddress = address + '\n' + city + ',' + state + ' ' + zip;
        return fullAddress;
    }
}

public class SiteOwner : Contact {
    public string FullAddress() {
        string fullAddress = base.FullAddress();

        // 执行一些其它操作
        return fullAddress;
    }
}

    在这个特定的例子中,SiteOwner类中的FullAddress()方法调用了Contact类中的FullAddress()方法。这通过一个对基类的引用来完成。这提供了另一种重用代码的途径,并添加了用户的行为。

版本

    版本——继承中的一个环境——在C#中是一种机制,可以修正类(建立新版本)但不致意外地改变了代码的意图。上面的隐藏基类成员方法的例子会得到编译器的一个警告,这正是由于C#的版本政策。它[译注:指版本机制]被设计用于消除修正基类时所带来的一些问题。

    考虑这样一幕:一个开发人员建立了一个类,从第三方的库中的类继承而来。最为讨论,我们不妨假设Contact类就是第三方类库中的类。看下面的例子:

public class Contact {
    // 不包括FullAddress()方法
}

public class SiteOwner : Contact {
    public string FullAddress() {
        string fullAddress = mySite.ToString();
        return fullAddress;
    }
}

    在这个例子中,基类中并没有FullAddress()方法。这还没有问题。稍后,第三方库的建立者更新了他们的代码。这些更新的部分中就包括了和派生类中的成员同名的成员:

public class Contact {
    private string address;
    private string city;
    private string state;
    private string zip;

    public string FullAddress() {
        string fullAddress = address + '\n' + city + ',' + state + ' ' + zip;
        return fullAddress;
    }
}

public class SiteOwner : Contact {
    public string FullAddress() {
        string fullAddress = mySite.ToString();
        return fullAddress;
    }
}

    在这段代码中,基类中的FullAddress()方法和派生类中的方法具有不同的功能。在其他语言中,由于隐式的多态性,这种情形将会破坏代码。然而,在C#中这不会破坏任何代码,因为当在SiteOwner上调用FullAdress()时,仍然调用的是SiteOwner类中的方法。

    但是这种情况会得到一个警告信息。消除这一警告消息的方法是在派生类的方法名字前面放置一个new修饰符,如下面例子所示:

using System;

public class WebSite {
    public string SiteName;
    public string URL;
    public string Description;

    public WebSite() {
    }

    public WebSite (
        string strSiteName,
        string strURL,
        string strDescription
    ) {
        SiteName = strSiteName;
        URL = strURL;
        Description = strDescription;
    }

    public override string ToString() {
        return SiteName + ", " + URL + ", " + Description;
    }
}

public class Contact {
    public string address;
    public string city;
    public string state;
    public string zip;

    public string FullAddress() {
        string fullAddress = address + '\n' + city + ',' + state + ' ' + zip;
        return fullAddress;
    }
}

public class SiteOwner : Contact {
    int siteHits;
    string name;
    WebSite mySite;

    public SiteOwner() {
        mySite = new WebSite();
        siteHits = 0;
    }

    public SiteOwner(string aName, WebSite aSite) {
        mySite = new WebSite (
            aSite.SiteName,
            aSite.URL,
            aSite.Description
        );

        Name = aName;
    }

    new public string FullAddress() {
        string fullAddress = mySite.ToString();
        return fullAddress;
    }

    public string Name {
        get {
            siteHits++;
            return name;
        }

        set {
            name = value;
            siteHits = 0;
        }
    }
}

public class Test {
    public static void Main() {
        WebSite mySite = new WebSite (
            "Le Financier",
            "http://www.LeFinancier.com",
            "Fancy Financial Site"
        );

        SiteOwner anOwner = new SiteOwner("John Doe", mySite);
        string address;

        anOwner.address = "123 Lane Lane";
        anOwner.city = "Some Town";
        anOwner.state = "HI";
        anOwner.zip = "45678";

        address = anOwner.FullAddress(); // 不同的结果
        Console.WriteLine("Address: \n{0}\n", address);
    }
}

输出为:

Address:
Le Financier, http://www.LeFinancier.com, Fancy Financial Site

    这具有让编译器知道开发者意图的效果。将new修饰符放到基类成员声明的前面表示开发者知道基类中有一个同名的方法,并且他们确实想隐藏这个成员。这可以保护那些依赖于基类成员实现的现存代码不受破坏。在C#中,当使用一个派生类对象时,调用的是基类的方法。同样,当调用基类成员的方法时,调用的也是基类的方法。但这会出现一个问题,就是如果基类中添加了一个重要的新特性,则这些新特性对派生类是无效的。

    要想使用这些新特性,就需要一些不同的途径。一种选择就是重命名派生类的成员,以允许程序通过派生类成员来使用一个基类方法。这种做法的缺点是,另一个依赖这个派生类实现的类可能具有同名的成员。这会破坏代码,因此,这是一种不好的形式。

    另一个选择是在派生类中定义一个新的方法来调用基类方法。这允许派生类的用户能够获得基类的新功能,同时保留了派生类现有的功能。尽管这可以工作,但会关系到派生类的可维护性。

封闭类

    封闭类是不可以继承的类。将一个类声明为封闭类可以保护这个类不被其他类继承。这样做有很多有益的原因,包括优化和安全性。

    封闭一个类可以皮面虚拟方法带来的系统开销。这允许编译器进行一些优化,这些优化对普通的类是无效的。

    使用封闭类的另一个有益的原因是安全性。继承,由于它的本质,可能以某些受保护成员能够访问到基类的内部。封闭一个类可以排除派生类的这些缺陷。关于封闭类的一个很好的例子是String类。下面的例子显示了如何建立一个封闭类:

public sealed class CustomerStats {
    string gender;
    decimal income;
    int numberOfVisits;

    public CustomerStats() {
    }
}

public class CustomerInfo : CustomerStats { // 错误
}

    这个例子将会产生编译错误。由于CustomerStats类是封闭的,它不能被CustomerInfo类继承。不过CustomerStats类可以用作一个类内部的对象。下面的就是在Customer类中声明了一个CustomerStats类的对象。

public class Customer {
    CustomerStats myStats; // 正确
}

多态性

    多态性反映了能够在多于一个类的对象中完成同一事物的能力——用同一种方法在不同的类中处理不同的对象。例如,如果CustomerVendor对象都有一个Name属性,则我们可以写一个事物来调用Name属性而不管我们所使用的是Customer对象还是Vendor对象,这就是多态性。

    交通工具是多态性的一个很好的例子。一个交通工具接口可以只包括所有交通工具都具有的属性和方法,还可能包括颜色、车门数、变速器和点火器等。这些属性可以用于所有类型的交通工具,包括轿车、卡车和挂车。

    多态性不在交通工具的属性和方法背后实现代码。相反,多态性只是实现接口。如果轿车、卡车和挂车都实现了同样的交通工具接口,则所有这三个类的客户代码是完全一样的。

    C#通过继承来为我们提供多态性。C#提供了virtual关键字用于定义一个支持多态的方法。

    子类对于虚拟方法可以自由地提供它自己的实现,这称为重写。下面是一些关于虚拟方法的要点:

要点:

  • 如果方法是非虚拟的,编译器简单地使用所引用的类型来调用适当的方法。
  • 如果方法是虚拟的,编译器将产生代码来在运行时检查所引用的类,来从适当的类型中调用适当的方法。
  • 当一个虚拟方法被调用时,运行时会进行检查(方法迟绑定)来确定对象和调用适当的方法,所有这些都是在运行时进行的。

    对于非虚拟方法,这些信息在编译期间都是无效的,因此不会引起运行时检查,所以调用非虚拟方法的效率会略微高一些。但在很多时候虚拟方法的行为会更有用,损失的那些性能所换来的功能是很值得的。

实现多态性

    实现多态性的关键因素是基于对象的类型动态地调用类的方法。本质上,一个程序会有一组对象,它会检查每一个对象的类型,并执行适当的方法。下面是一个例子:

using System;

public class WebSite {
    public string SiteName;
    public string URL;
    public string Description;

    public WebSite() {
    }

    public WebSite (
        string strSiteName,
        string strURL,
        string strDescription
    ) {
        SiteName = strSiteName;
        URL = strURL;
        Description = strDescription;
    }

    public override string ToString() {
        return SiteName + ", " + URL + ", " + Description;
    }
}

    当我们继承了上述的类,我们有两种方法来调用这个类的构造器。因此,这是一个设计时的多态。这时,我们必须在设计时决定在继承的类中调用哪一个方法。

    多态性是程序通过一个一般基类的引用来完成实现在多个派生类中的方法的能力。多态性的另一个定义是通过同一种方法来处理不同对象的能力。这意味着检测一个对象的行为是通过运行时类型,而不是它在设计时所引用的类型。

总结

    上面我尝试通过一些实际的例子解释了C#中面向对象的基本概念,但这将是一个长期的旅程。

你可能感兴趣的:(面向对象)