編程語言的一些基礎概念(三):面向對象

在前面兩篇中,主要講了函數式編程語言的一些基礎概念。這篇是 Coursera Programming Languages, Part C 的總結,通過 Ruby 介紹面向對象編程里的一些概念。了解這些概念能讓你在上手任何一門新的面向對象語言時,都更加得心應手。

雖然用的是 Ruby,但是不會涉及很深的 Ruby,即使不懂 Ruby,讀下來應該沒問題。對于已經了解面向對象編程的朋友,可以考慮直接跳到子類和繼承那部分,或許你會有一些新的啟發。

面向對象編程 & Ruby

面向對象編程(Object Oriented Programming)簡稱 OOP,像 Java,C++ 等語言的主要編程模式都是面向對象的。OOP 主要是通過 對象(Object) 來抽象表示,比如說平面上的一個點,可以抽象表示成 Point(x, y),x,y 表示這個點的橫縱坐標。在對象中可以有一些方法(methods) 來具體表示這個對象能做些什么,比如對于一個點,可定義一個 method distFromOrigin ,去求這個點的到原點的距離。

Everything is object in Ruby

Ruby 是動態類型的面向對象語言。Ruby 中所有的表達式都是一個對象,比如數字 1, 2, 3, 4 等是對象,+ 加法是對象的一個方法。Ruby 最出名的是在網頁應用的開發,但是因為 Everything is object in Ruby,被選做為這個課程的教學語言。

Class & Methods

在 OOP 中,最基本的就是怎么定義和使用對象以及對象中的方法。

定義 Class 和 Methods

Ruby 中,對象的定義通過 Class 實現。

class Foo
  def m1
    ...
  end
  def m2 (x,y)
    ...
  end
end

這里定義了一個對象 Foo,以及兩個方法 m1 和 m2。

調用方法

foo = Foo.new
foo.m1
foo.m2(x, y)

上例是最簡單的方法調用。

 e0.m(e1, ..., en)

這是調用方法的抽象表達,可以有另一種理解: 發送消息,e0 是消息的接受者,可以說將 e1 的結果作為參數放在消息 m 里,發送給 e0。

在 Ruby 中,所有表達式都是對象,e1 + e2 實際上是 e1.+ e2 的簡寫法,e1 調用了 方法 +,e2 是這個方法調用的參數。

定義 實例變量、類變量、類常量、類方法

class Foo
  Const = "constant"

  def initialize
    @foo = 1
    @@bar = 2

  def m1
    ...
  end

  def m2 (x,y)
    ...
  end

  def self.classMethod
    ...
  end
end

上例中,@f 為實例變量 (instance variable) ,@@f 為類變量 (class variable) ,Const 是類常量 (Class constant),classMethod 是類方法。

怎么使用類常量和類方法?

Foo::Const
Foo.classMethod

別名 Aliasing

foo = Foo.new
bar = foo

在這個例子中,bar 是 foo 的別名,他們對應是同一個 object,如果 bar 更改了,foo 也會相對應的更改。在 OOP 中,不像函數式編程里數據不可更改,需要特別注意什么時候用別名,什么時候要新建一個 object。

可見性 Visibility

對象內的變量和方法根據不同的定義方式,對象外不一定可見,可以理解為知不知道它的存在。

class Foo
  def initialize
    @foo = 1

  public
  def m1
    ...
  end

  protected
  def m2
    ...
  end

  private
  def m3
    ...
  end
end

foo = Foo.new
foo.@foo # 報錯
foo.m1   # 可見
foo.m2   # 報錯
foo.m3   # 報錯

實例變量 @foo 在 class Foo 外是不知道它的存在的,foo.@foo 會報錯,實例變量只有對象內的方法 m1, m2, m3 可以使用。

這樣的設計其實很符合面向對象的概念,Foo 是一個對象,要跟這個對象交流,只能通過發送消息,也就是調用方法的方式。

對象內的方法也可以定義可見性。

  • public,表示對外可見,Ruby 對象默認的模式,在類之外可以調用。
  • protected 和 private 只能在 類和子類都能調用。

Getter & Setter

對象的實例變量對外不可見,但是很多情況下可能會需要實例變量,這個時候可以通過定義 Getter 和 Setter 兩個方法來實現實例變量的讀寫。

# getter
def foo @foo
end

# setter
def foo= x
  @foo = x
end

# simpler way to define getters
attr_reader :y, :z # defines getters

# simpler way to define getters and setters
attr_accessor :x # defines getters and setters

反射 Reflections

反射指的是在程序運行的過程中,能訪問、檢測和修改它本身的這個對象。比如說可以在運行的過程中去訪問這個對象里都有哪些方法,然后動態的去調用這些方法。

鴨子類型 Duck Typing

當看到一只鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那么這只鳥就可以被稱為鴨子。

def mirror_update point
  point.x = point.x * -1
end

這個方法本意是將一個點 (point) 的 x 值變為 -x,但是所有"像點一樣有 x 這個方法"的對象都可以作為參數傳入這個函數,這就是所謂的鴨子類型。通常只有動態類型的語言才會支持鴨子類型,但是 Golang 也是有辦法實現,這里不展開了。

優缺點

鴨子類型的最大的優點在于代碼的重用,同樣一段代碼,傳進的參數只要有那些方法,就能夠復用這段代碼。但是也帶來了缺點,重用使得代碼表意不清,我們在用這個方法時,考慮的不是這個方法怎么調用,而是要清楚的了解這個方法的具體實現。舉個例子:

def double x
  x + x
end

看到 double 這個函數名,可能最直觀的理解就是把數字翻倍,具體的實現 x + x,也可以把兩個字符串連在一起。但假如 Ruby 的字符串沒有 * 這個方法,具體實現是 2*x 的話,這個函數就不適用于字符串。所以知道一個函數名不夠,還得看具體的實現,也挺麻煩的。

Blocks and Using Blocks

這是 Ruby 特有的一個特性。

def silly a
  (yield a) + (yield 42)
end

x.silly(5) {|b| b*2} # 5 * 2 + 42 * 2 = 94

{|b| b*2} 類似于這種的結構,在 Ruby 中是一個 Block,Block 可以放在任何方法調用后,通過方法里的 yeild 來調用 Block 里的內容。上例中,yield a 變成了 a * 2,也就是 5*2。如果一個方法里有 yield,就一定要傳入 Block,不然會報錯。

子類化和繼承

子類化 (SubClassing) 和 繼承 (Inheritence) 是 基于類的 OOP 里最重要也是最基礎的概念。如果一個類是另一個的子類,那么這個類的實例也是另一個的實例。

class Point
  attr_accessor :x, :y
  def initialize(x,y)
    @x = x
    @y = y end
  def distFromOrigin
    Math.sqrt(@x * @x  + @y * @y)
  end
end
class ColorPoint < Point
  attr_accessor :color
  def initialize(x,y,c="clear")
    super(x,y)
    @color = c
  end
end

在 Ruby 中,子類化用 < 來表示,ColorPoint 是 Point 的子類,它繼承了 Point 里所有的方法和變量,所以 ColorPoint 也有 distFromOrigin 的方法,也有 x, y 坐標。可以看出,通過這種繼承關系,ColorPoint 不用將 Point 里關于 x, y 及計算到頂點的距離的代碼再復制一遍,實現了代碼的重用。

覆寫和動態調度

覆寫 (Overriding) 指的是在子類里和父類一樣名字的方法會覆寫父類的方法。

動態調度 (Dynamic Patching) 也叫 late binding 或者 virtual methods,指的是從子類去調用父類方法,父類方法里可以動態的調度子類的方法。

class A
  def m1
    self.m2
  end
  def m2
    puts "m2 in A is called"
  end
  def m3
    puts "m3 in A is called"
  end
end

class B < A
  def dynamicDispatch
    self.m1
  end
  def m2
    puts "m2 in B is called"
  end
  def m3
    puts "m3 in B is called"
  end
end

b = B.new
b.m3 # m3 in B is called
b.dynamicDispatch # m2 in B is called

上例中,子類 B 中的方法 m3 覆寫了 A 中的 m3,b.dynamicDispatch 調用了 A 中的 m1, m1 里 self.m2的應該調用 A 還是 B 的 方法 m2 呢?因為發起調用的是 b,所以雖然調用的是 A 中 m1 的 self.m2,但是會被動態的調用到 B 中的 m2。

動態調度 VS Closure

對比下嗎 ML 和 Ruby 的兩個例子:

fun even x = if x=0 then true  else odd  (x-1)
and odd  x = if x=0 then false else even (x-1)

fun even x = (x mod 2)=0
fun even x = false

在 ML 中,有 closure,所以后面定義的兩個 even 函數,對第一個函數沒有任何影響。

class A
  def even x
    if x==0 then true  else odd  (x-1) end
  end
  def odd x
    if x==0 then false else even (x-1) end
end end
class B < A  # improves odd in B objects
  def even x ; x % 2 == 0 end
end
class C < A  # breaks odd in C objects
  def even x ; false end
end

B.new.odd
C.new.odd

因為動態調度 A 中的 even 不會被調用,而是調用子類 B 和子類 C 的 even。對于程序員來說,可以像 B 一樣,寫出更好的 even 的代碼,也可能像 C 一樣寫 bug。

可以說動態調度這個特性使得代碼更加靈活,重用率可能更高,給了程序員更大的自由度,但是需要程序員去注意不會一不小心寫了 bug。

方法查詢 Method Lookup

在覆寫和動態調度中,涉及到了 OOP 編程語言中怎么去查詢方法的設計。在 Ruby 中,對于一個 class C 的實例調用方法 m,方法查詢經過下面這些過程:

  1. 在 class C 中找方法 m
  2. 在 class C 中的 mixin (后面介紹) 中找方法 m
  3. 在 class C 的父類中找方法 m
  4. 在 class C 的父類的 mixin 中找方法 m
  5. 以此類推向上查找,直到找到方法 m 的定義,或者找不到返回 method missing error。

多方法 和 靜態重載

多方法 (Multimethods) ** 指的是在一個類里,有多個方法有一樣的名字,一樣數量的參數,但是參數的類型不一樣,在調用這個名字的方法時,會在動態**的找到最合適類型的那個方法,進行調用。Ruby 不支持 Multimethods,下面這個例子,借用 Ruby 的語法:

class A
  def addValue Int
    ...
  end
  def addValue String
    ...
  end
  def addValue Rational
    ...
  end
end

在調用時,A.new.addValue ?會根據參數類型,動態的找到最合適的方法。

Clojure 支持 Multimethods。

靜態重載 (Static Overload) 和多方法一樣,定義的多個方法名字是一樣的,但是可以是不同數量的參數和類型。和多方法最大的不同在于,重載不是動態調用時完成的,而是靜態的,感覺有點像類型檢查,在程序編譯時就完成了(不是很確定)。看一個 Java 的例子:

public class Sum {
    // Overloaded sum(). This sum takes two int parameters
    public int sum(int x, int y) {
        return (x + y);
    }

    // Overloaded sum(). This sum takes three int parameters
    public int sum(int x, int y, int z) {
        return (x + y + z);
    }

    // Overloaded sum(). This sum takes two double parameters
    public double sum(double x, double y) {
        return (x + y);
    }
}

多重繼承 Multiple Inheritence

多重繼承指的是繼承多個父類。看一個 C++ 的例子:

struct Base1 {
  void print (void) {
    std::cout << "Base 1" << std::endl;}
};

struct Base2 {
  void print (void) {
    std::cout << "Base 2" << std::endl;}
};

struct Derived1 : public Base1, public Base2 {
  void print (void) { // 重寫基類方法
    Base1::print(); // 指定使用何種
    Base2::print();
  }
}

// 原文:https://blog.csdn.net/caroline_wendy/article/details/18077235

多重繼承比只能有一個父類的繼承要復雜的多,比如多個父類的優先順序問題,多個父類中,都有同一個方法時,method lookup 要怎么行?類型檢查,class 的結構也要相對的復雜的多。所以不是所有的 OOP 都支持多重繼承,C++ 是支持的,但 Java 和 Ruby 就沒有。

Mixin

因為多繼承的復雜性,還有單繼承代碼復用的局限性,有了一個新的概念可以解決這個問題,mixin。Mixin 是一些方法的合集,可以被包含在 class 里,在這個 class 里可以直接用 mixin 里的方法。

module Doubler
  def double
    self + self # assume included in classes w/ +
  end
end
class String
  include Doubler
end
class Point
  attr_accessor :x, :y
  include Doubler
  def + other
    ans = Point.new
    ans.x = self.x + other.x
    ans.y = self.y + other.y
    ans
end

上例中,Doubler 是一個 Mixin 被用在了 String 和 Point 兩個類里,使得兩個類都有了 double 這個方法,然后在 class 里可以像 Point 里一樣去實現 double 里的 + 方法,來相加兩個 Point。再看一個例子:

# you define <=> and you get ==, >, <, >=, <= from the mixin
# (overrides Object's ==, adds the others)
class Name
  attr_accessor :first, :middle, :last
  include Comparable
  def initialize(first,last,middle="")
    @first = first
    @last = last
    @middle = middle
  end
  def <=> other
    l = @last <=> other.last # <=> defined on strings
    return l if l != 0
    f = @first <=> other.first
    return f if f != 0
    @middle <=> other.middle
  end
end

a = Name.new("Tom", "Li")
b = Name.new("Jame", "Chong")
a < b

include Comparable==, >, <, >=, <= 這些方法都包含進去了,在 Comparable 里用 <=> 這個方法返回的結果 -1, 0, 1 來決定 ``==, >, <, >=, <=` 這些返回 true 或者 false。

感覺是一個非常巧妙的做法,讓代碼的重用蹭蹭蹭往上漲。也很好的解決了在只有一個繼承的情況下,怎么去擴展一個 class 的實現。

接口 Interface

接口 (Interface) 是另外一個 OOP 中非常重要的概念,在接口里可以定義一些方法,但是這些方法只有名稱、參數類型還有返回類型,沒有方法的實現。接口的實現由別的類來完成。

interface Example {
  void   m1(int x, int y);
  Object m2(Example x, String y);
}

class A implements Example {
  public void m1(int x, int y) {...}
  public Object m2(Example e, String s) {...}
}

class B implements Example {
  public void m1(int pizza, int beer) {...}
  public Object m2(Example e, String s) {...}
}

在這個例子中,class A 和 class B 分別實現了 接口 Example,可以看出接口的一個大的優勢在于將實現和接口分離,可以很容易的更換實現,而不變接口,從客戶端來說,只要知道接口是什么樣的,不用管具體的實現。

像 Java 這類靜態類型的語言,因為有類型檢查,所以不像 Ruby 這種動態類型的語言那樣靈活,Interface 的另一個優點是讓這個語言有了更大的靈活性。這也是動態類型語言中沒有 Interface 的原因之一,因為那些語言本身已經非常靈活了。

抽象方法 Abstract Methods

抽象方法 (Abstract Methods) 是由父類定義了一些沒有實現的方法,具體實現由子類 (subclass) 去實現。

abstract class A {
  T1 m1(T2 x) { ... m2(e); ... }
  abstract T3 m2(T4 x);
}
class B extends A {
  T3 m2(T4 x) {
    ...
  }
}

上例中,父類 A 里定義了抽象方法 m2,只有參數類型和返回類型,所有繼承了 A 的子類,都需要實現 m2,比如 class B。

抽象方法、接口還有多重繼承,通常一門語言里不會都包括這三項。比如 C++ 中,只有抽象方法和多重繼承,所以可以定義抽象類,讓子類多重繼承,抽象類的作用就有點像 interface。

子類型 SubTyping

先看兩個類型定義:

1. (int, int, string)
2. (int, int)

在這例子中,1是2的子類型 (SubTyping)。如果一列類型通過省略掉其中零個或者幾個類型和另一列類型一模一樣,可以說這列類型是另一列類型的子類型。

子類型影響到的是靜態類型中的類型檢查,如果一個函數傳入的參數是要求參數的子類型,類型檢查該不該通過?

fun distToOrigin (p:{x:real,y:real}) = ...
fun makePurple (p:{color:string}) = ...
val c :{x:real,y:real,color:string} = {x=3.0, y=4.0, color="green"}
val _ = distToOrigin(c)
val _ = makePurple(c)

這是一個捏造的例子,distToOrigin(c)makePurple(c)該不該報錯呢?一門語言該不該接受子類型呢?要在什么樣的程度接受子類型?

在 OOP 中,SubClass 的類型其實就是父類的子類型。在 Java 中,是支持子類型的傳遞,也支持一個 list 子類型的傳遞,但是會產生一些問題:

class Point { ... }
class ColorPoint extends Point { ... }
...
void m1(Point[] pt_arr) {
  pt_arr[0] = new Point(3,4);
}
String m2(int x) {
  ColorPoint[] cpt_arr = new ColorPoint[x];
  for(int i=0; i < x; i++)
     cpt_arr[i] = new ColorPoint(0,0,"green");
  m1(cpt_arr); // !
  return cpt_arr[0].color; // !
}

在這個例子中,m1(cpt_arr) 是能通過類型檢查的,但是在 m1里 ColorPoint 被設置成了 Point,沒了 color 這個 attribute,在 cpt_arr[0].color 中就會出錯。Java 的處理方式是在雖然通過了類型檢查,但是 run-time 自在pt_arr[0] = new Point(3,4) 報錯 ArrayStoreException。

更加靈活的類型系統,需要更多更靈活的程序,但是同時能避免的錯誤也減少了。

另一個很有一些的是 null,在 Java 中本該不是對象,也沒有任何的方法,但是卻像是所有類型的"子類型",任何對象類型都可以以 null 傳入。

泛型 和 子類型

泛型 (Generics) 指的是能夠接受任何類型。

class Pair<T1,T2> {
  T1 x;
  T2 y;
  Pair(T1 _x, T2 _y){ x = _x; y = _y; }
  Pair<T2,T1> swap() {
     return new Pair<T2,T1>(y,x);
  }
  ...
}

這里 T1 和 T2 可以是任何類型。在一些情況下,我們可能會要求一個類型是任何子類型的泛型。比如下面這個例子:

List<Point> inCircle(List<Point> pts, Point center, double r) { ... }

List<Point> result = new ArrayList<Point>();
for(Point pt: pts)
  if(pt.distance(center) <= r)
    result.add(pt);
return result;

inCircle 參數和返回的類型是 Point 的 List,如果我們有 Point 的子類 ColorPoint,就得把這個代碼復制一遍。為了代碼復用,也不能把 List 設置成泛型,因為如果 List 不是 Point 的子類會出問題。有沒有辦法能實現一個只允許子類型的泛型?這叫做 限定多態 (bounded polymorphism)

<T extends Pt> List<T> inCircle(List<T> pts, Pt center, double r) {
   List<T> result = new ArrayList<T>();
   for(T pt: pts)
     if(pt.distance(center) <= r)
       result.add(pt);
   return result;
}

這里,通過 <T extends Pt> 實現了限定多態。

總結

在這篇文章中,介紹了很多面向對象類語言的基礎概念,比如方法/變量的可見性、子類、繼承、接口、抽象方法等等。不是每一門語言都包括了所有的這些概念,這里應該也只介紹了多數,不能說所有的概念。但是如果你好好讀了這篇文章,以后你在碰到任何一門面向對象的編程語言時,敢說一定有很多相似處,可以讓你更快的上手那門語言。通過 子類,多重繼承,Mixin,接口,抽象方法等的討論,也能讓你更好的理解那門語言的特性。

這篇文章是這個系列文章的最后一篇。三篇文章分別是對 Coursera Programming Languages 三個部分的總結,介紹了函數式編程和面向對象編程的一些基礎概念。這個應該是目前學到的 Coursera 上最棒的課,幫助我深入了解了 函數式編程 和 面向對象編程,雖然平時一直是在面向對象編程,但是從編程語言這個角度去解讀現在和以前用過的編程語言,還是學到了許多新內容,認識到了一門編程語言在設計的過程中,都有哪些取舍,能夠進一步去思考為什么這么取舍,這么取舍導致了這門語言的什么特性?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 找不到自己的世界,找不到自己存在的方式。像一條明明在水里的魚,在尋找自己的池塘。 在與自己養成的不良頑疾做苦苦的掙...
    和和菁音閱讀 317評論 0 0
  • 最后,我想就“生活的藝術”這一章談談自己的看法。縱觀全書,我覺得這一部分是最接地氣的一部分,它較為詳細地描述了中...
    隕落的小白閱讀 664評論 0 2
  • 星期六的早上,陽光明媚,東東和蘭蘭在公園的大樹下做早操,胸前的紅領巾在太陽的照射下更加鮮艷了!
    65dac2cad152閱讀 500評論 0 0