65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 和 Ruby 类

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.74/5 (16投票s)

2013年2月23日

CPOL

16分钟阅读

viewsIcon

58843

深入探讨 C# 和 Ruby 类之间的差异和相似之处。

(抱歉表格格式不佳 - 我正在努力改进并排比较。)

目录

引言

Ruby 是一种解释型面向对象语言,同时具有类似函数式语言的特性。  它也被称为鸭子类型语言。  来自 维基百科

在面向对象编程语言的计算机编程中,鸭子类型是一种动态类型风格,其中一个对象的行为和属性决定了有效的语义,而不是它继承自特定类或实现特定接口。该概念的名称指的是鸭子测试,该测试归功于 James Whitcomb Riley(见下文历史),可以表述如下:

当我看到一只像鸭子一样走路、像鸭子一样游泳、像鸭子一样叫的鸟时,我称那只鸟为鸭子。

在本文中,我将比较和对比 Ruby 类定义和 C#。  关于这个主题已经有无数的文章,例如 Brad Cunningham 的博客文章 Ruby for the C# developer - The Basics。  我在这里要努力做的是比通常在较短的博客条目中遇到的更全面的讨论。  为此,我借鉴了各种来源,这些来源我将在文章末尾的“参考文献”部分提供。  在整篇文章中,我也尽量遵守 C# 和 Ruby 的正确命名约定。  在 Ruby 中,类(被认为是常量)以大写字母开头。  其他所有内容都是小写字母,方法和属性的单词组用下划线分隔,例如“MyClass”和“my_method”。

Go to the head of the Class

“一个类不仅仅是一个定义,它也是一个活的对象。”5

两种语言中的类都有相似之处

  • 它们定义字段(通常是非公共的)
  • 它们定义属性来访问这些字段
  • 它们定义接受参数并操作这些字段的方法

然而,关于抽象、密封、内部等访问修饰符存在显著差异。  这些将首先讨论。

在 C# 和 Ruby 中定义一个空类

C# Ruby
class CSharpClass
{
}
class RubyClass
end

这里最显著的区别是使用“end”而不是花括号。

派生类

处理基类和派生类的语法是相似的

C# Ruby
class BaseClass
{
}

class DerivedClass : BaseClass
{
}
class BaseClass
end

class DerivedClass < BaseClass
end

构造函数

让我们先回顾一下构造函数,因为我将在演示 Ruby 中创建抽象类的一些技术时使用它们。  目前,让我们假设 C# 端是公共可访问的。

C# Ruby
public class BaseClass
{
  public BaseClass()
  {
    Console.WriteLine("BaseClass constructor.");
  }
}

public class DerivedClass : BaseClass
{
  public DerivedClass()
  {
    Console.WriteLine("DerivedClass constructor.");
  }
}

class Program
{
  static void Main(string[] args)
  {
    new DerivedClass();
  }
}
class BaseClass
  def initialize
    puts "BaseClass initializer"
  end
end



class DerivedClass < BaseClass
  def initialize
    puts "DerivedClass initializer"
  end
end




DerivedClass.new

Ruby 中不会自动调用基类构造函数

观察 C# 和 Ruby(正在运行 Interactive Ruby Console)的输出差异

C# Ruby

在 Ruby 中,必须使用“super”关键字显式调用基类构造函数

class DerivedClass < BaseClass
  def initialize
    super
    puts "DerivedClass initializer"
  end
end

这导致与 C# 相同的行为

类访问修饰符

C# 支持类上的以下访问修饰符

  • public - 该类可以被任何其他文件或程序集访问。  如果省略,则该类表现得好像指定了“internal”。
  • sealed - 防止其他类继承具有此访问修饰符的类。
  • internal - 将类可访问性限制在同一程序集内的文件。

在 Ruby 中,没有等价物 - public、private 和 protected 只应用于方法1。  正如关于 Ruby 所说,“在鸭子类型语言中,重点在于方法,而不是类/类型……”2

类类型修饰符

C# 类还允许两个类型修饰符

  • static - 表明类仅包含静态字段、属性和/或方法。 
  • abstract - 表明一个类仅用作其他类的基类。  抽象类不能直接实例化。

在 Ruby 中,也没有直接等同于 static 和 abstract 的。  然而,Ruby 类可以被强制转换为看起来像静态类或抽象类,我们将在下面探讨。

静态类:在 Ruby 中创建静态类

在 Ruby 中,静态变量使用双井号 (@@) 运算符指定,静态方法使用类名作为函数名前缀

C# Ruby
public static class StaticClass
{
  private static int foo = 1;

  public static int GetFoo()
  {
    return foo;
  }
}
class StaticClass
  @@foo = 1

  def StaticClass.get_foo
    @@foo
  end
end

如果调用 get_foo,则会产生预期的行为

抽象类:通过引发初始化异常来创建抽象类

一种方法是在初始化器(构造函数的等价物)中引发异常

class AbstractRubyClass
  def initialize
    if self.class == AbstractRubyClass
      raise "Cannot instantiate an abstract class."
    end
    puts "AbstractRubyClass initializer"
  end
end

class ConcreteRubyClass < AbstractRubyClass
  def initialize
    super
    puts "ConcreteRubyClass initializer"
  end
end

我们现在可以像往常一样实例化具体类

ConcreteRubyClass.new

结果是:

但是,如果我们尝试实例化抽象类,我们会收到一个异常

这种魔法之所以有效,是因为基类会验证类实例本身不是它自己——如果是,它就会引发异常。  对于创建抽象类的其他方法,请参阅 此处。  Ruby 的一个特点是通常有多种方法可以做同一件事,每种都有其优点和缺点。

使用 private_class_method 函数创建密封的单例类

在讨论了静态和抽象类型修饰符之后,现在似乎应该看看单例类。  通过更改“new”函数的可见性可以创建单例类3,这类似于 C# 类构造函数从 public 更改为 protected 或 private

C# Ruby
public class SingletonClass
{
  private SingletonClass() 
  { 
    Console.WriteLine("SingletonClass constructor.");
  }

  public static SingletonClass Create()
  {
    instance = instance ?? new SingletonClass();
    return instance;
  }
}
class SingletonClass
  private_class_method :new

  def initialize
    puts "SingletonClass initializer"
  end

  def SingletonClass.create(*args, &block)
    @@inst ||= new(*args, &block)
  end

end

请注意,上面的 Ruby 代码是一个静态方法的示例,该示例在本文章的末尾进行了讨论。  如果我们尝试实例化该类,则会导致错误,但使用静态创建方法时,初始化函数会被调用一次且仅一次

为了完整起见,这里是对一些 Ruby 语法的简要解释

  • *args - 以星号 (*) 开头的参数表示可变数量的参数
  • &block - 这本质上是一个 lambda 表达式,可以传递给构造函数
  • @@inst 是一个类变量(又名类的静态属性。)
  • ||= - 仅当左值是 nil 时,此赋值才将右侧的求值结果赋给左值
  • 与函数式语言类似,最后一个计算值会自动返回,因此不需要显式的 return 语句。

在 C# 中,如果构造函数是受保护的,则可以从单例类派生——因此,C# 单例类的构造函数实际上应该标记为 private。  但是,在 Ruby 中,即使 private 实际上更像是 C# 的 protected 可见性,上面的 Ruby 类也不能被派生。  例如,这个

class ConcreteRubyClass < SingletonClass
  def initialize
    super
    puts "ConcreteRubyClass initializer"
  end
end

导致运行时错误

我们有时会用类做的几件事

Robert Klemme 有一个出色的博客4,其中讨论了我们都理所当然的类的特性,以下四个部分就源于此。

将类转换为字符串

所有 C# 类型,因为它们都派生自 Object,所以都实现了 ToString()。  Ruby 中等价的是“to_s”

C# Ruby
public abstract class Vehicle
{
}

public class Car : Vehicle
{
  public override string ToString()
  {
    return "Car";
  }
}

public class Boat : Vehicle
{
  public override string ToString()
  {
    return "Boat";
  }
}

class Program
{
  static void Main(string[] args)
  {
    Console.WriteLine(new Boat().ToString());
  }
}
# Abstract Vehicle class
class Vehicle
  def initialize
    if self.class == Vehicle
      raise "Cannot instantiate an abstract class."
    end
  end
end

class Car < Vehicle
  def initialize
    super
  end

  def to_s
    "Car"
  end
end

class Boat < Vehicle
  def initialize
    super
  end

  def to_s
    "Boat"
  end
end

当然会导致

C# Ruby

等价性和哈希码

Ruby 有几种相等性测试,这可能会引起混淆。6  基本经验法则是“equal?”函数永远不应被覆盖,因为它用于确定对象标识 - 对象 A 和对象 B 是同一个实例。  与 C# 不同,在 C# 中,如果覆盖 == 时没有同时覆盖 Equals 方法,则会发出警告消息。  考虑一个“Engine”类的 C# 和 Ruby 实现,其中不同的实例被认为是等效的,如果汽缸数相同(是的,这是一个牵强的例子)

C# Ruby
public class Engine
{
  public int Cylinders { get; set; }

  public Engine(int numCyl)
  {
    Cylinders = numCyl;
  }

  public static bool operator ==(Engine a, Engine b)
  {
    return a.Cylinders == b.Cylinders;
  }

  public static bool operator !=(Engine a, Engine b)
  {
    return !(a==b);
  }

  public override bool Equals(object obj)
  {
    Engine e = obj as Engine;

    if ((object)e == null)
    {
      return false;
    }

    return Cylinders == e.Cylinders;
  }
 
  public override int GetHashCode()
  {
    return base.GetHashCode() ^ Cylinders.GetHashCode();
  }
}

class Program
{
  static void Main(string[] args)
  {
    Engine e1 = new Engine(4);
    Engine e2 = new Engine(4);
    Engine e3 = new Engine(6);

    Console.WriteLine(e1 == e2);
    Console.WriteLine(e1 == e3);
    Console.WriteLine(e1.Equals(e2));
    Console.WriteLine((object)e1 == (object)e2);
  }
}
class Engine
  attr_accessor :numCyl

  def initialize(numCyl)
    @numCyl = numCyl
  end

  def ==(other)
    self.class.equal?(other.class) &&
      @numCyl==other.numCyl
  end

  def hash
    super.hash ^ numCyl
  end
end

e1 = Engine.new(4)
e2 = Engine.new(4)
e3 = Engine.new(6)

e1==e2
e1==e3
e1.Equal?(e2)

导致输出

C# Ruby

请注意,在 C# 中,为了执行实例比较,必须将实例强制转换为对象,以免使用重写的 == 运算符,而且,Equals 运算符不再用于测试不同的实例。

请注意 Ruby 代码中的类型相同性检查,因为在 Ruby 中,我们可以传入不同类型的对象,如果它有 numCyl 属性,代码将愉快地与不同对象进行比较。

可比性

让我们快速看一下比较运算符。7  C# 中每个比较运算符 <、<=、>=、> 都需要单独覆盖,这在 Ruby 中要简单得多

C# Ruby
public class Engine
{
  ...
  public static bool operator <(Engine a, Engine b)
  {
    return a.Cylinders < b.Cylinders;
  }

  public static bool operator <=(Engine a, Engine b)
  {
    return a.Cylinders <= b.Cylinders;
  }

  public static bool operator >(Engine a, Engine b)
  {
    return a.Cylinders > b.Cylinders;
  }

  public static bool operator >=(Engine a, Engine b)
  {
    return a.Cylinders >= b.Cylinders;
  }
...
}
class Engine
  include Comparable
  ...
  def <=>(other)
    self.class == other.class ? 
        @numCyl <=> other.numCyl : nil
  end
end

Ruby 代码利用了“mixin”(稍后将详细介绍)和 <=> 运算符。

Dispose 和 Finalizers

C# 和 Ruby 都使用自动垃圾回收,而不是要求程序员进行显式取消分配调用。  这都没问题,直到需要释放非托管资源。  C# 提供了几个机制来实现这一点,最常见的是将使用非托管资源的代码包装在“using”块中——当块退出时,对象的析构函数会自动调用,假设类实现了 IDispose 接口,C# 中的正确实现10,Ruby 中的 finalizer 的正确实现11以及 dispose 的 hackish 实现(由我,从一些关于“using”的示例代码中借用13)如下所示

C# Ruby
public class DisposeMe : IDisposable
{
  private bool disposed = false;

  public DisposeMe()
  {
    Console.WriteLine("Constructor");
  }

  ~DisposeMe()
  {
    Console.WriteLine("Destructor");
    Dispose(false);
  }

  public void Dispose()
  {
    Console.WriteLine("Dispose");
    Dispose(true);
    GC.SuppressFinalize(this);
  }

  protected virtual void Dispose(bool disposing)
  {
    Console.WriteLine("Disposing. disposed = {0}, disposing = {1}",
      disposed, disposing);

    if (!disposed)
    {
      if (disposing)
      {
        Console.WriteLine("Free other managed resources");
      }

      Console.WriteLine("Free unmanaged resources");
      disposed = true;
    }
  }
}
# global method - needs to be defined only once somewhere
def using(object)
  yield object
ensure
  object.dispose
end

class DisposeMe
  @disposed = false

  def initialize
    ObjectSpace.define_finalizer(self, self.class.finalize(@disposed))
  end

  # pass in other instance specific params that are needed
  # to dispose of unmanaged objects.
  def self.finalize(disposed)
    proc {
      if !disposed
        dispose #pass in other args here
      else
        puts "object already disposed"
      end
    }
  end

  # class method called by GC. Pass in other required instance
  # variables here to dispose of unmanaged objects.
  def self.dispose
    puts "Disposing unmanaged objects"
  end

  # instance method called by using. We have access to instance
  # variables here
  def dispose
    self.class.dispose
    @disposed = false
  end
end

请注意,在 Ruby 代码中,我们需要类方法和实例方法“dispose”,因为 finalizer 的 proc 无法引用其自身的实例——否则对象将永远不会被收集。

一个简单的测试程序

C# Ruby
class Program
{
  static void Main(string[] args)
  {
    Console.WriteLine("Begin");
    
    using (DisposeMe d = new DisposeMe())
    {
      Console.WriteLine("Doing something with DisposeMe");
    }

    Console.WriteLine("End");
  }
}
using(DisposeMe.new) do |my_obj| puts "Doing something."; end

# simulate GC after using has disposed object
DisposeMe.finalize(true).call

# simulate GC when object hasn't been disposed
DisposeMe.finalize(false).call

生成(哈哈哈)

C# Ruby

构造期间的属性赋值

C# 中有时有用的语法是在类实例化期间初始化属性,而不是作为传递给构造函数的参数。  如果想在 Ruby 中这样做,只需在构造函数中添加一个块

C# Ruby
public class TwoNums
{
  public int A { get; set; }
  public int B { get; set; }

  public int Sum { get { return A + B; } }
}

class Program
{
  static void Main(string[] args)
  {
    TwoNums nums = new TwoNums() { A = 1, B = 5 };
    Console.WriteLine(nums.Sum);
  }
}  
class Adder
  attr_accessor :a
  attr_accessor :b
  
  def sum
    a+b
  end

  def initialize(&block)
    block.call(self) unless block.nil? 
  end
end

# Example usage:

adder = Adder.new do |this| this.a=1; this.b=5 end
adder.sum
adder2 = Adder.new

但是,这要求您创建专门具有调用块(表达式)能力的构造函数。  一个明显更复杂的15解决方案涉及重新定义类的“new”函数,我在此不赘述,并且要求声明“可初始化属性”,以便 new 函数可以确定哪些参数是初始化参数,哪些是构造函数参数。

接口

关于类的讨论如果没有涉及 C# 和 Ruby 中的接口概念,那就不完整了。  在 C# 中,接口强制执行一种契约:类 C,继承自接口 I,实现 I 中定义的所有属性和方法。  在 Ruby 中,接口是无意义的,因为方法可以动态地从类中添加和删除,即使它们在类中丢失,也可以在 Ruby 提供的“method_missing”钩子中处理。8  Ruby 的动态特性使得无法强制执行实现契约(接口)。 

多重继承(混入和模块)

C# 完全不支持多重继承。  接口并不是实现多重继承的真正方式,因为接口仅仅是一个定义,而不是一个实现。  Ruby 不支持显式形式的多重继承(如“Car < Vehicle, OperatorLicense”)然而,多重继承的原则(从多个源继承实现)是通过模块和 mixin9实现的。  例如

require "date"
# Abstract Vehicle class
class Vehicle
  def initialize
    if self.class == Vehicle
      raise "Cannot instantiate an abstract class."
    end
  end
end

module OperatorLicense
  attr_accessor :licenseNo
  attr_accessor :expirationDate
end

class Car < Vehicle
  include OperatorLicense
  def initialize
    super
  end

  def to_s
    "#{licenseNo} expires #{expirationDate.strftime("%m/%d/%Y")}"
  end
end

通过“混入”模块“OperatorLicense”,我们获得了属性(又称属性)“licenseNo”和“expirationDate”,我们现在可以在 Car 子类中使用它们

car = Car.new
car.licenseNo = "1XYZ30"
car.expirationDate=DateTime.new(2016, 12, 31)

结果是:

这是扩展 Ruby 类行为的强大方式,您会经常看到它(例如,上面使用的 Comparable 就是一个 mixin。)

字段和属性

C# 中的属性由显式或隐式字段支持,这些字段在 Ruby 中被称为“attributes”。 

字段

在下面的示例中,字段的使用在 C# 和 Ruby 之间是等效的

C# Ruby
public class Person
{
  private string firstName;
  private string lastName;

  public Person(string first, string last)
  {
    firstName = first;
    lastName = last;
  }

  public string FullName()
  {
    return firstName + " " + lastName;
  }
}
class Person
  def initialize(first, last)
    @first_name = first
    @last_name = last
  end

  def full_name
    @first_name + " " + @last_name
  end
end

当然,请注意,在 Ruby 中不需要声明字段。

属性

类似地,属性在 C# 和 Ruby 中都是方法。  如果我们忽略两种语言中的语法糖,属性声明如下

C# Ruby
public class Person
{
  private string firstName;
  private string lastName;

  public string get_firstName() {return firstName;}
  public void set_firstName(string name) {firstName=name;}

  public string get_lastName() { return lastName; }
  public void set_lastName(string name) { lastName = name; }

  public Person(string first, string last)
  {
    firstName = first;
    lastName = last;
  }

  public string FullName()
  {
    return firstName + " " + lastName;
  }
}
class Person
  def first_name
    @first_name
  end

  def first_name=(name)
    @first_name=name
  end

  def last_name
    @last_name
  end

  def last_name=(name)
    @last_name=name
  end

  def initialize(first, last)
    @first_name = first
    @last_name = last
  end

  def full_name
    @first_name + " " + @last_name
  end
end

这显然不是两种语言中通常使用的语法。  我们通常看到的更像是

C# Ruby
public class Person
{
  public string FirstName { get; set; }
  public string LastName { get; set; }

  public Person(string first, string last)
  {
    FirstName = first;
    LastName = last;
  }

  public string FullName()
  {
    return FirstName + " " + LastName;
  }
}
class Person
  attr_accessor :first_name
  attr_accessor :last_name

  def initialize(first, last)
    @first_name = first
    @last_name = last
  end

  def full_name
    @first_name + " " + @last_name
  end
end

使用方式正如我们所预期的

C# Ruby
Person p = new Person("Marc", "Clifton");
Console.WriteLine(p.FullName());
p.FirstName = "Ian";
Console.WriteLine(p.FullName());
p = Person.new("Marc", "Clifton")
p.full_name
p.first_name="Ian"
p.full_name

只读属性

C# 中一种常见的形式是将 setter 标记为 protected,使其不可写(这与“readonly”属性不同)。  Ruby 中存在类似的概念

C# Ruby
public string FirstName { get; protected set; }
public string LastName { get; protected set; }
attr_reader :first_name
attr_reader :last_name

在 Ruby 中,具有相同访问级别的属性可以分组在一起,例如

attr_reader :first_name, :last_name

此外,在 Ruby 中,有一个“attr_writer”,在 C# 中等同于以下内容

C# Ruby
public string FirstName { protected get; set; }
public string LastName { protected get; set; }
attr_writer :first_name
attr_writer :last_name

事实上,Ruby 中的 attr_accessor 等同于声明 attr_reader 和 attr_writer。

只读字段

C# 的 readonly 属性可以实现为防止对字段进行赋值。  Ruby 中没有等价物——不要将“freeze”函数与 C# 的 readonly 属性混淆。  Ruby 的 freeze 函数阻止对象被修改,它不阻止同一对象被赋给新值。

C# Ruby
public class ReadOnlyExample
{
  // Can be initialized in the declaration.
  public readonly int SomeValue = 13;

  public ReadOnlyExample()
  {
    // Can be assigned in the constructor
    SomeValue = 15;
  }

  public void AttemptToModify()
  {
    // but no further assignments are allowed.
    SomeValue = 3;
  }
}
class ReadOnlyExample
  attr_accessor :some_value

  def initialize
    @some_value = 15
    @some_value.freeze
  end

  def attempt_to_modify
    @some_value = 3
  end
end

a = ReadOnlyExample.new
a.attempt_to_modify
a.some_value

在 C# 中导致编译器错误

error CS0191: A readonly field cannot be assigned to (except in a constructor or a variable initializer)

但在 Ruby 中却产生了完全有效的执行

静态属性(类属性)

以上描述了“实例属性”,并且在两种语言中都支持静态属性,在 Ruby 中也称为类变量。  在下面的示例中,我向 Person 类型添加了一个静态属性来计算实例化次数

C# Ruby
public class Person
{
  public static int TotalPeople { get; set; }
  public string FirstName { protected get; set; }
  public string LastName { protected get; set; }

  public Person(string first, string last)
  {
    FirstName = first;
    LastName = last;
    ++TotalPeople;
  }

  public string FullName()
  {
    return FirstName + " " + LastName;
  }
}

class Program
{
  static void Main(string[] args)
  {
    Person p1 = new Person("Marc", "Clifton");
    Person p2 = new Person("Ian", "Clifton");
    Console.WriteLine(Person.TotalPeople);
  }
}
class Person
  @@total_people = 0
  attr_accessor :first_name, :last_name

  def self.total_people
    @@total_people
  end

  def initialize(first, last)
    @first_name = first
    @last_name = last
    @@total_people += 1
  end

  def full_name
    @first_name + " " + @last_name
  end
end

p1 = Person.new("Marc", "Clifton")
p2 = Person.new("Ian", "Clifton")
Person.total_people
#also
Person::total_people

注意函数“total_people”中的“self.”,它表示函数与 Person 类的实例 p1 和 p2 相关联。  Ruby 代码也可以这样写

def Person.total_people
  @@total_people
end

然而,“self”正变得越来越流行。  另外,请考虑此语法

class Person
  class << self
    attr_accessor :total_people
  end

  def initialize(first, last)
    @first_name = first
    @last_name = last
    Person.total_people ||= 0
    Person.total_people += 1
  end

  def full_name
    @first_name + " " + @last_name
  end
end

p1 = Person.new("Marc", "Clifton")
p2 = Person.new("Ian", "Clifton")
Person.total_people

在此,“class << self”语法是一个单例声明,指定属性和方法属于类定义而不是类实例,并且可以通过“Person.”构造函数访问(“self.”不再有效)。

编写自己的访问器

在 Ruby 中,我们也可以通过将“attr_static_accessor”的定义添加到 Class 类型来编写自己的“attr_static_accessor”12

class Class
  def attr_static_accessor(class_name, *args)
    args.each do |arg|
      self.class_eval("def #{class_name}.#{arg};@@#{arg};end")
      self.class_eval("def #{class_name}.#{arg}=(val);@@#{arg}=val;end")
    end
  end
end

class Person
  attr_static_accessor(:Person, :total_people)

  def initialize(first, last)
    @first_name = first
    @last_name = last
    @@total_people ||= 0
    @@total_people += 1
  end

  def full_name
    @first_name + " " + @last_name
  end
end

p1 = Person.new("Marc", "Clifton")
p2 = Person.new("Ian", "Clifton")
Person.total_people

这产生了预期的行为

计算性getter 和 setter

在 C# 中,计算通常放在属性 getter 和 setter 中。  显然,在 Ruby 中,您只需编写函数

C# Ruby
public class Person
{
  public string FirstName { get; protected set; }
  public string LastName { get; protected set; }

  public string FullName
  {
    get { return FirstName + " " + LastName; }
    set
    {
      string[] names = value.Split(' ');
      FirstName = names[0];
      LastName = names[1];
    }
  }
}

class Program
{
  static void Main(string[] args)
  {
    Person p = new Person() {FullName = "Marc Clifton"};
    Console.WriteLine(p.FirstName);
    Console.WriteLine(p.LastName);
    Console.WriteLine(p.FullName);
  }
}
class Person
  attr_accessor :first_name, :last_name

  def full_name=(value)
    names = value.split(" ")
    @first_name=names[0]
    @last_name=names[1]
  end

  def full_name
    @first_name + " " + @last_name
  end
end

p = Person.new
p.full_name = "Marc Clifton"
p.first_name
p.last_name
p.full_name


结果是

C# Ruby

但是,如果您想变得花哨,可以通过将表达式作为字符串传递并使用自定义属性访问器来在 Ruby 中实现等效。  首先,我们定义一个“attr_computed_accessor”,它将可用于所有类。

class Class
  def attr_computational_accessor(props)
    props.each { |prop, fnc|
      if fnc.key?(:getter)
        self.class_eval("def #{prop};#{fnc[:getter]};end")
      end
      if fnc.key?(:setter)
        self.class_eval("def #{prop}=(value);#{fnc[:setter]};end")
      end
    }
  end
end

现在我们可以编写一个测试类

class ComputationalProperty
  #expose attributes set by computation
  attr_accessor :first_name
  attr_accessor :last_name

  attr_computational_accessor(:full_name => {
    :getter => %{@first_name + " " + @last_name},
    :setter => %{
      names=value.split(" ")
      @first_name = names[0]
      @last_name = names[1]
      }
  })
end

请注意,上面的代码虽然看起来像 lambda 表达式,但实际上并不是——它是一个传递给 class_eval 函数的字符串。

结果如下:

如果您想使用 lambda 表达式,您必须传入上下文14。  属性的定义方式如下

class Class
  def attr_computational_accessor(props)
    props.each { |prop, fnc|
      if fnc.key?(:getter)
        self.class_eval do
          define_method prop do
            instance_eval do fnc[:getter].call(self) end
          end
        end
      end
      if fnc.key?(:setter)
        self.class_eval do
          define_method "#{prop}=".to_sym do |value|
            instance_eval do fnc[:setter].call(self, value) end
          end
        end
      end
    }
  end
end

要求指定上下文来访问实例属性和方法,我将其表示为参数“this”

class ComputationalProperty
  #exposes attributes set by computation
  attr_accessor :first_name
  attr_accessor :last_name
  attr_computational_accessor(:full_name => {
    :getter => lambda {|this| this.first_name + " " + this.last_name},
    :setter => lambda {|this, value|
      names = value.split(" ")
      this.first_name = names[0]
      this.last_name = names[1]
    }
  })
end

这里是另一个使用示例,仅说明 getter

class AnotherComputer
  attr_accessor :a
  attr_accessor :b
  attr_computational_accessor(:sum => {:getter => lambda {|this| this.a + this.b}})
end

Example usage

事件

C# 中另一个常见的做法是提供事件,用于在属性值更改时触发。  例如,这里有一个类,它会触发一个“changing”事件,为事件处理程序提供旧值和新值。  为了与 Ruby 进行比较,我利用了 Ruby gem“ruby_events”16

C# Ruby
public class PropertyChangingEventArgs<T> : EventArgs
{
  public T OldValue { get; set; }
  public T NewValue { get; set; }
}

public delegate void PropertyChangingDlgt<T>(object sender, 
                         PropertyChangingEventArgs<T> args);

public class Things
{
  public event PropertyChangingDlgt<int> AChanging;
  private int a;

  public int A
  {
    get { return a; }
    set
    {
      if (a != value)
      {
        if (AChanging != null)
        {
          AChanging(this, new PropertyChangingEventArgs<int>() 
             { OldValue = a, NewValue = value });
        }

        a = value;
      }
    }
  }
}
require 'rubygems'
require 'ruby_events'

class Things
  def A
    @a
  end

  def A=(value)
    if (value != @a)
      events.fire(:a_changing, self, @a, value)
    end

    @a=value
  end
end



请注意,我刚才破坏了 Ruby 代码中的命名约定样式——“A”函数实际上应该是“a”。

然后进行测试

C# Ruby
class Program
{
  static void Main(string[] args)
  {
    Things things = new Things() { A = 2 };

    things.AChanging += (sender, changeArgs) => 
    { 
      Console.WriteLine("Old Value = {0}, New Value = {1}",
             changeArgs.OldValue, changeArgs.NewValue);
    };

   things.A=5;
  }
}

这很容易。  还有许多其他方法可以做到这一点17,但 ruby_events gem 的一个有趣特性是您可以将回调注入现有类方法。  例如,属性 setter 不需要显式引发事件

class OtherThings
  attr_accessor :b
end

o = OtherThings.new
o.b = 2
o.events.fire_on_method(:b=.to_sym, :injected)
o.events.listen(:injected) do |event_data| puts "b is now " + event_data.to_s end
o.b = 5
b is now 5
=> 5

这还有什么比这更酷的!

方法

我们已经看到了各种各样的方法——属性 getter、setter 和一些简单函数。  这里还有几个概念需要涵盖。

静态方法

静态方法的前缀可以是“self.”或类名。  例如,这些是等效的

class StaticMethods
  def self.first_static_method
    puts "first"
  end

  def StaticMethods.second_static_method
    puts "second"
  end
end

用法就像 C# 一样

抽象方法

如果您想强制执行抽象行为,您可以定义一个类级别函数,如果在调用时意外调用它,则引发异常,从而强制在派生类中实现(当然,由于 Ruby 是解释型语言,这发生在运行时。)

class Class
  def abstract(*methods)
    methods.each do |m| define_method(m) {raise "Abstract method #{m} called."} end
  end
end

其用法如下

class AbstractMethods
  abstract :A1, :A2
end

class ConcreteMethods < AbstractMethods
  def A1
    puts "A1"
  end

  # oops, didn't implement A2
end

生成以下输出

Method Missing

“所有 RubyEvents 功能都可通过 object.events 调用。”16

是的,方法实际上是消息,这为在类中缺少方法时创建新行为打开了大门。  所以让我们使用 method missing 功能将罗马数字转换为其阿拉伯数字的等价物(大量借鉴 代码 19

class RomanConverter
  @@data = [
    ["M" , 1000],
    ["CM" , 900],
    ["D" , 500],
    ["CD" , 400],
    ["C" , 100],
    ["XC" , 90],
    ["L" , 50],
    ["XL" , 40],
    ["X" , 10],
    ["IX" , 9],
    ["V" , 5],
    ["IV" , 4],
    ["I" , 1]
  ]

  def self.to_arabic(rom)
    result = 0
    for key, value in @data
      while rom.index(key) == 0
        result += value
        rom.slice!(key)
      end
    end
  result
end

  def self.method_missing(sym, *args)
    # no validation!
    to_arabic(sym.to_s)
  end
end

现在我们可以使用罗马数字作为方法名并返回阿拉伯数字

测试

本质上,问题是,“如果它像鸭子一样走路,像鸭子一样叫”,它可能是一条正在模仿鸭子的龙。  即使龙能模仿鸭子,您也不一定总是想让它们进入池塘。

鸭子类型的倡导者,如 Guido van Rossum,认为这个问题可以通过测试来处理,并且需要对代码库进行维护所需的知识。 鸭子类型) 

如果您阅读了本文的任何部分,您可能会立即被 Ruby 的灵活性所打动,并且由于它是一种鸭子类型语言,因此没有编译时类型检查——如果存在类型冲突、缺少实现等,这些可能在您运行程序时被发现——如果龙能叫,它仍然不是鸭子。  此外,Ruby 惊人的元编程能力18也不会在运行时揭示问题。  出于这个原因,编写测试的必要性怎么强调都不为过。  在开发 Ruby 应用程序时,应为每个新行为和现有行为的更改逐步且立即地添加测试。  如果您未能做到这一点,您很可能会在几个月后遇到运行时错误,因为某个依赖于已不存在或工作方式不同的代码分支出错了。  两个流行的测试工具是 RSpecCucumber

参考文献

1 - http://rubylearning.com/satishtalim/ruby_access_control.html
2 - http://stackoverflow.com/questions/512466/how-to-implement-an-abstract-class-in-ruby
3 - https://wikibooks.cn/wiki/Ruby_Programming/Syntax/Classes
4 - http://blog.rubybestpractices.com/posts/rklemme/018-Complete_Class.html
5 - http://openmymind.net/2010/6/25/Learning-Ruby-class-self/
6 - http://stackoverflow.com/questions/7156955/whats-the-difference-between-equal-eql-and
7 - http://blog.rubybestpractices.com/posts/rklemme/018-Complete_Class.html
8 - http://briancarper.net/blog/226/
9 - http://www.ruby-doc.org/docs/ProgrammingRuby/html/tut_modules.html
10 - http://lostechies.com/chrispatterson/2012/11/29/idisposable-done-right/
11 - http://www.mikeperham.com/2010/02/24/the-trouble-with-ruby-finalizers/
12 - http://mikeyhogarth.wordpress.com/2011/12/01/creating-your-own-attr_accessor-in-ruby/
13 - http://usrbinruby.net/ruby-general/2010-01/msg01369.html
14 - http://stackoverflow.com/questions/7470508/ruby-lambda-context
15 - http://stackoverflow.com/questions/1077949/best-way-to-abstract-initializing-attributes
16 - https://github.com/nathankleyn/ruby_events
17 - http://thatextramile.be/blog/2010/08/using-c-style-events-in-ruby
18 - http://yehudakatz.com/2009/11/15/metaprogramming-in-ruby-its-all-about-the-self/
19 - http://www.rubyquiz.com/quiz22.html

© . All rights reserved.