将Ruby转换为C#

7
需要将以下代码从Ruby转换为C#。然而,我对yield关键字的使用和Ruby的一般语法感到困惑。是否有了解Ruby的人可以帮忙转换代码?
class < < Cache
STALE_REFRESH = 1
STALE_CREATED = 2

# Caches data received from a block
#
# The difference between this method and usual Cache.get
# is following: this method caches data and allows user
# to re-generate data when it is expired w/o running
# data generation code more than once so dog-pile effect
# won't bring our servers down
#
def smart_get(key, ttl = nil, generation_time = 30.seconds)
  # Fallback to default caching approach if no ttl given
  return get(key) { yield } unless ttl

  # Create window for data refresh
  real_ttl = ttl + generation_time * 2
  stale_key = "#{key}.stale"

  # Try to get data from memcache
  value = get(key)
  stale = get(stale_key)

  # If stale key has expired, it is time to re-generate our data
  unless stale
    put(stale_key, STALE_REFRESH, generation_time) # lock
    value = nil # force data re-generation
  end

  # If no data retrieved or data re-generation forced, re-generate data and reset stale key
  unless value
    value = yield
    put(key, value, real_ttl)
    put(stale_key, STALE_CREATED, ttl) # unlock
  end

  return value
end

结束

4个回答

14

我完全不了解C#,所以我对C#的任何评论都应该持保留态度。然而,我将尝试解释一下这段Ruby代码中发生了什么。

class << Cache

Ruby有一种叫做“单例方法”的东西。它们与单例软件设计模式无关,只是为一个对象定义的方法。因此,您可以拥有相同类的两个实例,并向其中一个对象添加方法。
有两种不同的单例方法语法。一种是仅使用对象前缀方法名称,例如def foo.bar(baz)将只为对象foo定义一个bar方法。另一种方法称为“打开单例类”,它在语法上看起来类似于定义类,因为在类层次结构中,单例方法实际上位于对象和其实际类之间插入的一个不可见类中。
这个语法看起来像这样:class << foo。这打开了对象foo的单例类,该类体内定义的每个方法都成为对象foo的单例方法。
为什么在这里使用它?好吧,Ruby是一种纯面向对象的语言,这意味着所有东西,包括类都是一个对象。现在,如果可以向单个对象添加方法,并且类是对象,则意味着可以向单个类添加方法。换句话说,Ruby不需要常规方法和静态方法之间的人为区别(它们是欺诈,无论如何:它们实际上不是方法,只是光荣的程序)。在C#中的静态方法只是类对象的单例类上的常规方法。
所有这些都只是解释一切定义在class << Cache和其对应的end之间的内容都成为static的冗长方式。
  STALE_REFRESH = 1
  STALE_CREATED = 2

在Ruby中,每个以大写字母开头的变量实际上都是常量。然而,在这种情况下,我们不会将它们翻译为static const字段,而是作为enum来使用,因为这就是它们的用途。
  # Caches data received from a block
  #
  # The difference between this method and usual Cache.get
  # is following: this method caches data and allows user
  # to re-generate data when it is expired w/o running
  # data generation code more than once so dog-pile effect
  # won't bring our servers down
  #
  def smart_get(key, ttl = nil, generation_time = 30.seconds)

这种方法有三个参数(实际上是四个,我们稍后会看到为什么),其中两个是可选的(ttl和generation_time)。它们都有默认值,但在ttl的情况下,默认值并没有真正使用,它更像是一个标记,用于确定是否传递了参数。

30.secondsActiveSupport库添加到Integer类的扩展。它实际上并没有做任何事情,只是返回self。在这种情况下,它只是为了使方法定义更易读而使用。(还有其他一些方法可以做更有用的事情,例如Integer#minutes,它返回self * 60,以及Integer#hours等等。)我们将使用此作为指示,参数的类型不应该是int,而应该是System.TimeSpan

    # Fallback to default caching approach if no ttl given
    return get(key) { yield } unless ttl

这段内容包含了一些复杂的Ruby结构。我们先从最简单的开始:尾部条件修饰符。如果一个条件语句体只包含一个表达式,那么条件可以附加到表达式的末尾。因此,不必使用if a > b then foo end,也可以使用foo if a > b。所以,上面的代码等同于unless ttl then return get(key) { yield } end

下一个也很简单:unless只是if not的语法糖。因此,现在我们有了if not ttl then return get(key) { yield } end

第三个是Ruby的真值系统。在Ruby中,真值非常简单。实际上,假值非常简单,而真值则自然而然地出现:特殊关键字false是假的,特殊关键字nil是假的,其他所有值都是真的。因此,在这种情况下,只有当ttlfalsenil时,条件才会成立。对于时间跨度来说,false不是一个合理的值,因此唯一有意义的是nil。这段代码更清晰的写法应该是:if ttl.nil? then return get(key) { yield } end。由于ttl参数的默认值为nil,因此如果没有传递ttl参数,则条件成立。因此,条件用于确定方法被调用时使用了多少个参数,这意味着我们不会将其翻译为条件语句,而是作为方法重载。

现在,让我们来看看yield。在Ruby中,每个方法都可以接受一个隐式代码块作为参数。这就是我上面所说的方法实际上需要四个参数而不是三个的原因。代码块只是一段匿名的代码,可以传递、存储在变量中,并在以后调用。Ruby从Smalltalk继承了块的概念,但这个概念可以追溯到1958年的Lisp的lambda表达式。在提到匿名代码块时,至少现在你应该知道如何表示这个隐式的第四个方法参数:一个委托类型,更具体地说,是一个Func

那么,yield是什么?它将控制权转移到块中。它基本上只是一种非常方便的调用块的方式,而无需显式地将其存储在变量中,然后调用它。

    # Create window for data refresh
    real_ttl = ttl + generation_time * 2
    stale_key = "#{key}.stale"

这个#{foo}语法被称为字符串插值。它的意思是“用大括号内表达式求值的结果替换字符串中的标记”。它只是String.Format()的一个非常简洁的版本,我们正好要将其翻译成这个。

    # Try to get data from memcache
    value = get(key)
    stale = get(stale_key)

    # If stale key has expired, it is time to re-generate our data
    unless stale
      put(stale_key, STALE_REFRESH, generation_time) # lock
      value = nil # force data re-generation
    end

    # If no data retrieved or data re-generation forced, re-generate data and reset stale key
    unless value
      value = yield
      put(key, value, real_ttl)
      put(stale_key, STALE_CREATED, ttl) # unlock
    end

    return value
  end
end

这是我尝试将Ruby版本翻译成C#的微小努力:
public class Cache<Tkey, Tvalue> {
    enum Stale { Refresh, Created }

    /* Caches data received from a delegate
     *
     * The difference between this method and usual Cache.get
     * is following: this method caches data and allows user
     * to re-generate data when it is expired w/o running
     * data generation code more than once so dog-pile effect
     * won't bring our servers down
    */
    public static Tvalue SmartGet(Tkey key, TimeSpan ttl, TimeSpan generationTime, Func<Tvalue> strategy)
    {
        // Create window for data refresh
        var realTtl = ttl + generationTime * 2;
        var staleKey = String.Format("{0}stale", key);

        // Try to get data from memcache
        var value = Get(key);
        var stale = Get(staleKey);

        // If stale key has expired, it is time to re-generate our data
        if (stale == null)
        {
            Put(staleKey, Stale.Refresh, generationTime); // lock
            value = null; // force data re-generation
        }

        // If no data retrieved or data re-generation forced, re-generate data and reset stale key
        if (value == null)
        {
            value = strategy();
            Put(key, value, realTtl);
            Put(staleKey, Stale.Created, ttl) // unlock
        }

        return value;
    }

    // Fallback to default caching approach if no ttl given
    public static Tvalue SmartGet(Tkey key, Func<Tvalue> strategy) => 
        Get(key, strategy);

    // Simulate default argument for generationTime
    // C# 4.0 has default arguments, so this wouldn't be needed.
    public static Tvalue SmartGet(Tkey key, TimeSpan ttl, Func<Tvalue> strategy) => 
        SmartGet(key, ttl, new TimeSpan(0, 0, 30), strategy);

    // Convenience overloads to allow calling it the same way as 
    // in Ruby, by just passing in the timespans as integers in 
    // seconds.
    public static Tvalue SmartGet(Tkey key, int ttl, int generationTime, Func<Tvalue> strategy) => 
        SmartGet(key, new TimeSpan(0, 0, ttl), new TimeSpan(0, 0, generationTime), strategy);

    public static Tvalue SmartGet(Tkey key, int ttl, Func<Tvalue> strategy) => 
        SmartGet(key, new TimeSpan(0, 0, ttl), strategy);
}

请注意,我不懂C#,不懂.NET,我没有测试过这个代码,甚至不知道它是否符合语法规则。但还是希望能对您有所帮助。

5

看起来这段代码被传递了一个块,如果缓存中没有请求的数据,则对其进行评估(yield是调用该块的方式)。这是相当惯用的Ruby代码;我不知道你是否可以“翻译”成C#。

寻找用例以了解我的意思。您应该找到某些模糊类似于此的内容:

x = smart_get([:foo,"bar"]) { call_expensive_operation_foo("bar") }

更好的方法是确定您需要它执行的功能,然后使用C#编写新代码,而不是尝试从Ruby进行“翻译”。

在 .NET >= 3.0 中,可以使用 lambda 表达式(参见 http://msdn.microsoft.com/en-us/library/bb397687.aspx)来实现类似的功能。 - Greg Campbell
你可以这样做,但是将充满惯用语的代码从一种语言翻译成另一种语言通常不会像你期望的那样顺利。它总是会变得有点像日本的“叮咚”笑话。 - MarkusQ

4

看起来你想把Ruby的memcache-client移植到C#上。如果是这样,使用本地的C# memcache客户端实现可能会更容易,例如:

http://code.google.com/p/beitmemcached/

无论如何,我基本上同意MarkusQ的观点,从高级语言到低级语言的翻译可能总体上不如以目标语言的习惯方式进行重写。直接从Ruby翻译到C#最好情况下会给你带来一些非常丑陋的代码。

yield Ruby关键字允许您调用作为隐式声明参数传递的代码块。因此,例如,您可以将smart_get方法定义视为实际上更像是:

def smart_get(key, ttl = nil, generation_time = 30.seconds, &block)

当您这样调用smart_get时:

x = smart_get("mykey", my_ttl) { do_some_operation_here }

花括号中的内容会被赋值给上述扩展定义中的变量块。而yield会调用&block中的代码。这只是一个粗略的概括,但应该有助于您获得大致的想法。
回到你的转换问题上。我刚刚做的简化并不能保证100%的成功,因为一旦你找到了将那段代码转换为C#语言的方法,另一段代码就可能会破坏你的翻译。例如,假设某个方法需要检查该块:
def foo
   if block.arity == 0
      # No arguments passed, load defaults from config file and ignore call
   else
      yield
   end
end

当然,由于 Ruby 中的 lambda 函数是一等对象,因此您可能会遇到以下类似的代码:
foo = lambda { |a, b, c| a + b + c }   
# foo is now defined as a function that sums its three arguments

如果你遇到定义动态方法的代码,或者(对于翻译人员来说更糟糕的是)利用Ruby的可塑性类的代码,那么上帝保佑你。

class Foo
   def show
      puts "Foo"
   end
end

foo = Foo.new
foo.show   # prints "Foo"

class <&lt;foo; def show; puts "Bar";  end; end

foo.show  # prints "Bar"

Foo.new.show  # prints "Foo"

foo.show  # Still prints "Bar"

由于Ruby中每个类的每个实例都可以有自己的类定义,因此即使是简单的示例也很难在C#中进行移植。Ruby具有许多这些特性,这会破坏简单的翻译工作,因此建议先学习要移植的内容,然后从头开始重新制作。


0

试试这个:

def foo
   if block.arity == 0
      # No arguments passed, load defaults from config file and ignore call
   else
      yield
   end
end

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接