设为首页 收藏本站
开启辅助访问 快捷导航
菜单
猿人部落 主页 资讯 查看内容

编程语言的一些基础概念(三):面向对象

2019-7-26 18:09 发布者: 木子独飞 评论 0 查看 828
版权声明:署名,允许他人基于本文举行创作,且必须基于与原先允许协议雷同的允许协议分发本文 (Creative Commons)
知识共享允许协议 版权声明:署名,允许他人基于本文举行创作,且必须基于与原先允许协议雷同的允许协议分发本文 (Creative Commons)

在前面两篇中,告急讲了函数式编程语言的一些根本概念。这篇是 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;
}

这里,通过 实现了限定多态。

总结

在这篇文章中,先容了许多面向对象类语言的根本概念,比如方法/变量的可见性、子类、继续、接口、抽象方法等等。不是每一门语言都包罗了全部的这些概念,这里应该也只先容了多数,不能说全部的概念。但是假如你好好读了这篇文章,以后你在遇到任何一门面向对象的编程语言时,敢说肯定有许多相似处,可以让你更快的上手那门语言。通过 子类,多重继续,Mixin,接口,抽象方法等的讨论,也能让你更好的明白那门语言的特性。

这篇文章是这个系列文章的末了一篇。三篇文章分别是对 Coursera Programming Languages 三个部分的总结,先容了函数式编程和面向对象编程的一些根本概念。这个应该是现在学到的 Coursera 上最棒的课,资助我深入相识了 函数式编程 和 面向对象编程,固然寻常不停是在面向对象编程,但是从编程语言这个角度去解读现在和从前用过的编程语言,还是学到了许多新内容,熟悉到了一门编程语言在操持的过程中,都有哪些弃取,可以大概进一步去思考为什么这么弃取,这么弃取导致了这门语言的什么特性?



路过

雷人

握手

鲜花

鸡蛋
收藏 邀请
上一篇:启航 —— 记 —— 第二次自考的反思:自考与自我改造的困境下一篇:开启技术之旅——我的第一篇博客

相关阅读

一周热门

头条攻略!

日排行榜

相关分类