Ruby - 如果变量不是数组,则优雅地将其转换为数组

149

给定一个数组、单个元素或nil,获取一个数组 - 后两者分别为一个含有单个元素和一个空数组。

我曾错误地认为Ruby会按照这种方式工作:

[1,2,3].to_a  #= [1,2,3]     # Already an array, so no change
1.to_a        #= [1]         # Creates an array and adds element
nil.to_a      #= []          # Creates empty array

但实际获得的是:

[1,2,3].to_a  #= [1,2,3]         # Hooray
1.to_a        #= NoMethodError   # Do not want
nil.to_a      #= []              # Hooray

所以要解决这个问题,我要么需要使用另一种方法,要么可以通过修改我打算使用的所有类的to_a方法来进行元编程 - 但这对我来说不是一个选项。

那么就用一个方法吧:

result = nums.class == "Array".constantize ? nums : (nums.class == "NilClass".constantize ? [] : ([]<<nums))

问题在于它有些混乱。是否有一种优雅的方法可以解决这个问题?(如果这是解决此问题的Ruby方式,我会感到惊讶)


这有什么应用?为什么要转换为一个数组?

在 Rails 的 ActiveRecord 中,调用例如 user.posts 将返回一个帖子数组、单个帖子或 nil。在编写对其结果进行操作的方法时,最容易假设该方法将使用一个数组,该数组可能有零个、一个或多个元素。示例方法:

current_user.posts.inject(true) {|result, element| result and (element.some_boolean_condition)}

2
user.posts 永远不应该返回单独的帖子,至少我从未见过这种情况。 - Sergio Tulentsev
1
我认为在你的前两个代码块中,你是想用 == 而不是 =,对吧? - Patrick Oscity
2
可能重复:https://dev59.com/AHRC5IYBdhLWcg3wK96V - Dan Grahn
3
顺便说一句,[1,2,3].to_a 不会 返回[[1,2,3]]!它返回的是 [1,2,3] - Patrick Oscity
谢谢Paddle,我会更新问题... 自己给自己脸掌 - xxjjnn
显示剩余2条评论
10个回答

175

[*foo]Array(foo) 大部分情况下都可以工作,但对于一些像哈希(hash)这样的情况,它会搞乱它。

Array([1, 2, 3])    # => [1, 2, 3]
Array(1)            # => [1]
Array(nil)          # => []
Array({a: 1, b: 2}) # => [[:a, 1], [:b, 2]]

[*[1, 2, 3]]    # => [1, 2, 3]
[*1]            # => [1]
[*nil]          # => []
[*{a: 1, b: 2}] # => [[:a, 1], [:b, 2]]

我能想到的即便是针对哈希的唯一方法,也是定义一个方法。
class Object; def ensure_array; [self] end end
class Array; def ensure_array; to_a end end
class NilClass; def ensure_array; to_a end end

[1, 2, 3].ensure_array    # => [1, 2, 3]
1.ensure_array            # => [1]
nil.ensure_array          # => []
{a: 1, b: 2}.ensure_array # => [{a: 1, b: 2}]

2
改用 to_a 方法替代 ensure_array 方法 - Dan Grahn
10
这将影响那些依赖于原始to_a使用的方法。例如,{a:1, b:2}.each...将会有不同的表现。 - sawa
1
你能解释一下这个语法吗?在多年的Ruby编程中,我从未遇到过这种类型的调用。类名后面的括号有什么作用?我在文档中找不到相关说明。 - mastaBlasta
1
@mastaBlasta Array(arg) 尝试通过调用 to_ary,然后对参数调用 to_a 来创建新数组。这在官方的 Ruby 文档中有详细说明。我是从 Avdi 的《自信的 Ruby》一书中学到的。 - mambo
4
在我发布问题后的某个时刻,我找到了答案。困难之处在于它与 Array 类无关,而是 Kernel 模块上的一个方法。http://ruby-doc.org/core-2.3.1/Kernel.html#method-i-Array - mastaBlasta
显示剩余7条评论

146

使用 ActiveSupport(Rails):Array.wrap

Array.wrap([1, 2, 3])     # => [1, 2, 3]
Array.wrap(1)             # => [1]
Array.wrap(nil)           # => []
Array.wrap({a: 1, b: 2})  # => [{:a=>1, :b=>2}]

如果您未使用Rails,您可以定义自己的方法,类似于Rails源代码中的方法。

class Array
  def self.wrap(object)
    if object.nil?
      []
    elsif object.respond_to?(:to_ary)
      object.to_ary || [object]
    else
      [object]
    end
  end
end

14
为了让代码更可爱,可以这样写:class Array; singleton_class.send(:alias_method, :hug, :wrap); end。该代码的作用是给数组类(Array)添加一个名为"hug"的别名方法,它和原有的"wrap"方法功能相同。 - rthbound

21

最简单的解决方案是使用[foo].flatten(1)。与其他提出的解决方案不同,它可以很好地处理(嵌套的)数组、哈希和nil

def wrap(foo)
  [foo].flatten(1)
end

wrap([1,2,3])         #= [1,2,3]
wrap([[1,2],[3,4]])   #= [[1,2],[3,4]]
wrap(1)               #= [1]
wrap(nil)             #= [nil]
wrap({key: 'value'})  #= [{key: 'value'}]

不幸的是,与其他方法相比,这个方法存在严重的性能问题。Kernel#ArrayArray()是最快的方法。Ruby 2.5.1的比较结果如下:Array(): 7936825.7 i/s. Array.wrap: 4199036.2 i/s - 慢了1.89倍。 wrap: 644030.4 i/s - 慢了12.32倍。 - Wasif Hossain

20

Array(whatever) 就可以解决问题。

Array([1,2,3]) # [1,2,3]
Array(nil) # []
Array(1337)   # [1337]

14
无法用于哈希表。Array({a: 1, b: 2}) 会变成 [[:a, 1], [:b, 2]]。 - davispuh

13

ActiveSupport(Rails)

ActiveSupport有一个非常不错的方法来实现这一点。它已经被Rails加载,所以绝对是最好的方法:

Array.wrap([1, 2, 3]) #=> [1, 2, 3]
Array.wrap(nil) #=> nil

Splat(Ruby 1.9+)

星号操作符(*)可以将数组还原为单个元素,如果可能的话:

*[1,2,3] #=> 1, 2, 3 (notice how this DOES not have braces)

当然,如果没有一个数组,这个操作会有奇怪的行为,并且你需要将你要"展开"的对象放在数组中。这听起来有些奇怪,但它的意思是:

[*[1,2,3]] #=> [1, 2, 3]
[*5] #=> [5]
[*nil] #=> []
[*{meh: "meh"}] #=> [[:meh, "meh"], [:meh2, "lol"]]

如果您没有ActiveSupport,您可以定义该方法:

class Array
    def self.wrap(object)
        [*object]
    end
end

Array.wrap([1, 2, 3]) #=> [1, 2, 3]
Array.wrap(nil) #=> nil

如果你计划有大量的数组和较少的非数组元素,你可能需要进行更改 - 上述方法在处理大型数组时速度较慢,甚至可能导致堆栈溢出(哦,如此元)。无论如何,你可能想使用以下方法:

class Array
    def self.wrap(object)
        object.is_a? Array ? object : [*object]
    end
end

Array.wrap([1, 2, 3]) #=> [1, 2, 3]
Array.wrap(nil) #=> [nil]

我也有一些包含或不包含三元运算符的基准测试


无法处理大数组。对于100万个元素(ruby 2.2.3),会出现“SystemStackError:堆栈级别太深”的错误。 - denis.peplin
@denis.peplin 似乎你遇到了一个 StackOverflow 错误 :D - 老实说,我不确定发生了什么。抱歉。 - Ben Aubin
我最近尝试使用Hash#values_at和1M个参数(使用splat),但它抛出了相同的错误。 - denis.peplin
@denis.peplin 这个代码能用于 object.is_a? Array ? object : [*object] 吗? - Ben Aubin
1
Array.wrap(nil) 返回的是 [] 而不是 nil :/ - Aeramor

7
如何呢?
[].push(anything).flatten

2
是的,我想在我的情况下最终使用了 [anything].flatten... 但对于一般情况,这也会使任何嵌套的数组结构变平。 - xxjjnn
2
[].push(anything).flatten(1) 可以实现!但是它不会将嵌套的数组展开! - xxjjnn

2

显而易见,虽然这段代码不是最美味的语法糖,但它似乎确实可以做到您所描述的功能:

foo = foo.is_a?(Array) ? foo : foo.nil? ? [] : [foo]

1
你可以重写 Object 的数组方法。
class Object
    def to_a
        [self]
    end
end

所有东西都继承自Object,因此to_a现在将为太阳下的所有东西定义


4
渎神的猴子补丁!忏悔吧! - xxjjnn

1
我已经浏览了所有答案,但大多数不适用于Ruby 2+。
但是elado提供了最优雅的解决方案,即:

使用ActiveSupport(Rails):Array.wrap

Array.wrap([1, 2, 3]) # => [1, 2, 3]

Array.wrap(1) # => [1]

Array.wrap(nil) # => []

Array.wrap({a: 1, b: 2}) # => [{:a=>1, :b=>2}]

可惜的是,这也不适用于Ruby 2+,因为你会得到一个错误。
undefined method `wrap' for Array:Class

所以为了解决这个问题,你需要使用以下代码:

require 'active_support/deprecation'

require 'active_support/core_ext/array/wrap'


0

由于两个主要有问题的类(NilHash)已经存在方法 #to_a,因此只需通过扩展 Object 来定义其余类的方法:

class Object
    def to_a
        [self]
    end
end

然后你可以轻松地在任何对象上调用该方法:

"Hello world".to_a
# => ["Hello world"]
123.to_a
# => [123]
{a:1, b:2}.to_a
# => [[:a, 1], [:b, 2]] 
nil.to_a
# => []

6
我认为最好避免对核心的Ruby类进行Monkey Patching,尤其是Object类。不过,我会放过ActiveSupport这个库,所以你可以说我有点虚伪。@sawa提供的解决方案比这更可行。 - pho3nixf1re

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