C# 和 Ruby 类






4.74/5 (16投票s)
深入探讨 C# 和 Ruby 类之间的差异和相似之处。
(抱歉表格格式不佳 - 我正在努力改进并排比较。)
目录
- 引言
- Go to the head of the Class
- 字段和属性
- 事件
- 方法
- 测试
- 参考文献
引言
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 应用程序时,应为每个新行为和现有行为的更改逐步且立即地添加测试。 如果您未能做到这一点,您很可能会在几个月后遇到运行时错误,因为某个依赖于已不存在或工作方式不同的代码分支出错了。 两个流行的测试工具是 RSpec 和 Cucumber。
参考文献
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