ruby 数组的 Uniq 无法工作

发布于 2024-12-25 16:39:08 字数 2698 浏览 0 评论 0原文

我有一个对象 Country 的数组,其中包含属性“code”和“name”

该数组中可能多次包含一个国家/地区,因此我想区分该数组。

这是我的国家类

class Country
  include Mongoid::Fields::Serializable
  attr_accessor :name, :code

  FILTERS = ["Afghanistan","Brunei","Iran", "Kuwait", "Libya", "Saudi Arabia", "Sudan", "Yemen", "Britain (UK)", "Antarctica", "Bonaire Sint Eustatius & Saba", "British Indian Ocean Territory", "Cocos (Keeling) Islands", "St Barthelemy", "St Martin (French part)", "Svalbard & Jan Mayen","Vatican City"]

  EXTRAS = {
    'eng' => 'England',
    'wal' => 'Wales',
    'sco' => 'Scotland',
    'nlr' => 'Northern Ireland'
    }

  def initialize(name, code)
    @name = name
    @code = code
  end

  def deserialize(object)
    return nil unless object
    Country.new(object['name'], object['code'])
  end

  def serialize(country)
    {:name => country.name, :code => country.code}
  end

  def self.all
    add_extras(filter(TZInfo::Country.all.map{|country| to_country country})).sort! {|c1, c2| c1.name <=> c2.name}
  end

  def self.get(code)
    begin
      to_country TZInfo::Country.get(code)
    rescue TZInfo::InvalidCountryCode => e
      'InvalidCountryCode' unless EXTRAS.has_key? code
      Country.new EXTRAS[code], code
    end
  end

  def self.get_by_name(name)
    all.select {|country| country.name.downcase == name.downcase}.first
  end

  def self.filter(countries)
    countries.reject {|country| FILTERS.include?(country.name)}
  end

  def self.add_extras(countries)
    countries + EXTRAS.map{|k,v| Country.new v, k}
  end

  private
  def self.to_country(country)
    Country.new country.name, country.code
  end
end

和我对从另一个类调用的数组的请求

  def countries_ive_drunk
    (had_drinks.map {|drink| drink.beer.country }).uniq
  end

如果我抛出数组,我可以看到结构是:

[
#<Country:0x5e3b4c8 @name="Belarus", @code="BY">, 
#<Country:0x5e396e0 @name="Britain (UK)", @code="GB">, 
#<Country:0x5e3f350 @name="Czech Republic", @code="CZ">, 
#<Country:0x5e3d730 @name="Germany", @code="DE">, 
#<Country:0x5e43778 @name="United States", @code="US">, 
#<Country:0x5e42398 @name="England", @code="eng">, 
#<Country:0x5e40f70 @name="Aaland Islands", @code="AX">, 
#<Country:0x5e47978 @name="England", @code="eng">, 
#<Country:0x5e46358 @name="Portugal", @code="PT">, 
#<Country:0x5e44d38 @name="Georgia", @code="GE">, 
#<Country:0x5e4b668 @name="Germany", @code="DE">, 
#<Country:0x5e4a2a0 @name="Anguilla", @code="AI">, 
#<Country:0x5e48c98 @name="Anguilla", @code="AI">
]

这是相同的,无论我是否执行 .uniq 并且您可以看到有两个“安圭拉” ”

I have a array of my object Country which has the attributes "code" and "name"

The array could have a country in it more than once so I want to distinct the array.

This is my countries class

class Country
  include Mongoid::Fields::Serializable
  attr_accessor :name, :code

  FILTERS = ["Afghanistan","Brunei","Iran", "Kuwait", "Libya", "Saudi Arabia", "Sudan", "Yemen", "Britain (UK)", "Antarctica", "Bonaire Sint Eustatius & Saba", "British Indian Ocean Territory", "Cocos (Keeling) Islands", "St Barthelemy", "St Martin (French part)", "Svalbard & Jan Mayen","Vatican City"]

  EXTRAS = {
    'eng' => 'England',
    'wal' => 'Wales',
    'sco' => 'Scotland',
    'nlr' => 'Northern Ireland'
    }

  def initialize(name, code)
    @name = name
    @code = code
  end

  def deserialize(object)
    return nil unless object
    Country.new(object['name'], object['code'])
  end

  def serialize(country)
    {:name => country.name, :code => country.code}
  end

  def self.all
    add_extras(filter(TZInfo::Country.all.map{|country| to_country country})).sort! {|c1, c2| c1.name <=> c2.name}
  end

  def self.get(code)
    begin
      to_country TZInfo::Country.get(code)
    rescue TZInfo::InvalidCountryCode => e
      'InvalidCountryCode' unless EXTRAS.has_key? code
      Country.new EXTRAS[code], code
    end
  end

  def self.get_by_name(name)
    all.select {|country| country.name.downcase == name.downcase}.first
  end

  def self.filter(countries)
    countries.reject {|country| FILTERS.include?(country.name)}
  end

  def self.add_extras(countries)
    countries + EXTRAS.map{|k,v| Country.new v, k}
  end

  private
  def self.to_country(country)
    Country.new country.name, country.code
  end
end

and my request for the array which is called from another class

  def countries_ive_drunk
    (had_drinks.map {|drink| drink.beer.country }).uniq
  end

If I throw the array I can see the structure is:

[
#<Country:0x5e3b4c8 @name="Belarus", @code="BY">, 
#<Country:0x5e396e0 @name="Britain (UK)", @code="GB">, 
#<Country:0x5e3f350 @name="Czech Republic", @code="CZ">, 
#<Country:0x5e3d730 @name="Germany", @code="DE">, 
#<Country:0x5e43778 @name="United States", @code="US">, 
#<Country:0x5e42398 @name="England", @code="eng">, 
#<Country:0x5e40f70 @name="Aaland Islands", @code="AX">, 
#<Country:0x5e47978 @name="England", @code="eng">, 
#<Country:0x5e46358 @name="Portugal", @code="PT">, 
#<Country:0x5e44d38 @name="Georgia", @code="GE">, 
#<Country:0x5e4b668 @name="Germany", @code="DE">, 
#<Country:0x5e4a2a0 @name="Anguilla", @code="AI">, 
#<Country:0x5e48c98 @name="Anguilla", @code="AI">
]

This is the same, whether or not I do .uniq and you can see there is two "Anguilla"

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

北斗星光 2025-01-01 16:39:09

至少早在 1.9.3 版本中,Array#uniq 就会像 uniq_by 一样占用一个块。 uniq_by 现已弃用。

At least as early as 1.9.3, Array#uniq would take a block just like uniq_by. uniq_by is now deprecated.

韬韬不绝 2025-01-01 16:39:08

正如其他人所指出的,问题在于 uniq 使用 hash 来区分国家/地区,并且默认情况下,Object#hash 对于所有国家/地区都是不同的对象。如果两个对象返回相同的 hash 值,它还会使用 eql? 来确定它们是否是 eql。

最好的解决办法就是首先让你的班级正确!

class Country
  # ... your previous code, plus:

  include Comparable

  def <=>(other)
    return nil unless other.is_a?(Country)
    (code <=> other.code).nonzero? || (name <=> other.name)
    # or less fancy:
    #   [code, name] <=> [other.code, other.name]
  end

  def hash
    [name, code].hash
  end

  alias eql? ==
end

Country.new("Canada", "CA").eql?(Country.new("Canada", "CA")) # => true

现在,您可以对国家/地区数组进行排序,使用国家/地区作为哈希值的键,比较它们等等...

我已经包含了上面的代码来展示它的一般情况,但在您的情况下,如果您愿意,您可以免费获得所有这些子类 Struct(:code, :name)...

class Country < Stuct(:name, :code)
  # ... the rest of your code, without the `attr_accessible` nor the `initialize`
  # as Struct provides these and `hash`, `eql?`, `==`, ...
end

As pointed out by others, the problem is that uniq uses hash to distinguish between countries and that by default, Object#hash is different for all objects. It will also use eql? in case two objects return the same hash value, to be sure if they are eql or not.

The best solution is to make your class correct in the first place!

class Country
  # ... your previous code, plus:

  include Comparable

  def <=>(other)
    return nil unless other.is_a?(Country)
    (code <=> other.code).nonzero? || (name <=> other.name)
    # or less fancy:
    #   [code, name] <=> [other.code, other.name]
  end

  def hash
    [name, code].hash
  end

  alias eql? ==
end

Country.new("Canada", "CA").eql?(Country.new("Canada", "CA")) # => true

Now you can sort arrays of Countries, use countries as key for hashes, compare them, etc...

I've included the above code to show how it's done in general, but in your case, you get all this for free if you subclass Struct(:code, :name)...

class Country < Stuct(:name, :code)
  # ... the rest of your code, without the `attr_accessible` nor the `initialize`
  # as Struct provides these and `hash`, `eql?`, `==`, ...
end
鸩远一方 2025-01-01 16:39:08

如果数组中的对象的 #hash 值重复,则 Array#uniq 会将其视为重复,但此代码中的情况并非如此。您需要使用不同的方法来完成预期的任务,如下所示:

def countries_ive_drunk
  had_drinks.map {|drink| drink.beer.country.code }
    .uniq
    .map { |code| Country.get code}
end

Objects in array are considered duplicate by Array#uniq if their #hash values are duplicate, which is not the case in this code. You need to use different approach to do what intended, like this:

def countries_ive_drunk
  had_drinks.map {|drink| drink.beer.country.code }
    .uniq
    .map { |code| Country.get code}
end
仅冇旳回忆 2025-01-01 16:39:08

这归结为平等意味着什么?什么时候一个对象是另一个对象的复制品? ==、eql 的默认实现?只需比较 ruby​​ object_id 这就是为什么你没有得到你想要的结果。

你可以实现 ==, eql 吗?并以对您的班级有意义的方式进行哈希处理,例如通过比较国家/地区的代码。

另一种方法是使用 uniq_by。这是对 Array 的主动支持补充,但 mongoid 无论如何都依赖于主动支持,因此您不会添加依赖项。

some_list_of_countries.uniq_by {|c| c.code}

将使用国家/地区代码来区分它们。您可以将其缩短为

 some_list_of_countries.uniq_by(&:code)

This boils down to what does equality mean? When is an object a duplicate of another? The default implementations of ==, eql? just compare the ruby object_id which is why you don't get the results you want.

You could implement ==, eql? and hash in a way that makes sense for your class, for example by comparing the countries' codes.

An alternative is to use uniq_by. This is an active support addition to Array, but mongoid depends on active support anyway, so you wouldn't be adding a dependency.

some_list_of_countries.uniq_by {|c| c.code}

Would use countries' codes to uniq them. You can shorten that to

 some_list_of_countries.uniq_by(&:code)
浮萍、无处依 2025-01-01 16:39:08

数组中的每个元素都是单独的类实例。

#<Country:0x5e4a2a0 @name="Anguilla", @code="AI"> 
#<Country:0x5e48c98 @name="Anguilla", @code="AI">

id 是唯一的。

Each element in the array is separate class instance.

#<Country:0x5e4a2a0 @name="Anguilla", @code="AI"> 
#<Country:0x5e48c98 @name="Anguilla", @code="AI">

The ids are unique.

病女 2025-01-01 16:39:08
#<Country:0x5e4a2a0 @name="Anguilla", @code="AI">, 
#<Country:0x5e48c98 @name="Anguilla", @code="AI">

Array#uniq 认为这些是不同的对象(Country 类的不同实例),因为对象的 id 不同。
显然你需要改变你的策略。

#<Country:0x5e4a2a0 @name="Anguilla", @code="AI">, 
#<Country:0x5e48c98 @name="Anguilla", @code="AI">

Array#uniq thinks these are different objects (different instances of Country class), because the objects' ids are different.
Obviously you need to change your strategy.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文