问题
Suppose I have an object:
obj = Object.new #<Object:0x00007fbe36b4db28>
And I convert it to an Enumerator:
obj_enum = obj.to_enum #<Enumerator: #<Object:0x00007fbe36b4db28>:each>
Now I want to get my object back from the enumerator. I found a way to do it, but it seems unnecessarily abstruse (not to mention pretty fragile):
extracted_obj = ObjectSpace._id2ref(
obj_enum.inspect.match(/0x[0-9a-f]*/).values_at(0)[0].to_i(16)/2
)
p obj.equal? extracted_obj # => true
In case it isn't clear, I'm inspecting the Enumerator object, using regex to pull the original object's id from the resulting string, converting it to an integer (and dividing by 2), and using ObjectSpace._id2ref
to convert the id to a reference to my object. Ugly stuff.
I have trouble believing that this is the easiest way to get this job done, but some hours of googling haven't revealed anything to me. Is there a simple way to extract an object after wrapping an Enumerator around it with #to_enum
, or is this pretty much the way to do it?
Edit:
As Amadan says below (and much appreciated, Amadan), this may be an XY problem, and I may have to rethink my solution. I'll explain a bit about how I got here.
The (meta) use case: I have a (variable) quantity of objects in an array. The objects each expose an array of integers (all of the same size) as an attribute, sorted high to low. I want to iterate the arrays of each of the objects simultaneously, finding the object or objects with the highest integer not matched in another object's array.
It seemed like external iteration was a good way to go about doing this, since simultaneous internal iteration of multiple objects that have to know about the intermediate results of one another's iterations gets out there pretty quickly as well. But when I have found the enumerator that contains the object with the array with the highest value, I need to return the object that it wraps.
There very well may be a better way to go than using enumerators when this is a requirement. However, the actual iteration and selection process is pretty trivial when using them.
So. The applied use case: a quantity of poker hands with no hand better than a high card. Find the winning hand. The "winning hand" is the hand with the highest card not matched in rank by another hand. (Suits are irrelevant.) If all the cards match in two or more hands, return those hands in an array.
"Minimal reproducible example":
class Hand
attr_reader :cards
def initialize(cards)
@cards = cards.sort.reverse
end
def each
@cards.each { |card| yield(card.first) }
end
end
class Poker
def initialize(hands)
@hands = hands.map { |hand| Hand.new(hand) }
end
def high_cards
hand_enums = @hands.map(&:to_enum)
loop do
max_rank = hand_enums.map(&:peek).max
hand_enums.delete_if { |enum| enum.peek != max_rank }
hand_enums.each(&:next)
end
hand_enums.map { |e| from_enum(e).cards }
end
def from_enum(enum)
ObjectSpace._id2ref(
enum.inspect.match(/0x[0-9a-f]*/).values_at(0)[0].to_i(16) / 2
)
end
end
hands = [
[[10, "D"], [3, "C"], [8, "C"], [7, "C"], [9, "D"]],
[[10, "D"], [8, "S"], [7, "S"], [9, "H"], [2, "H"]],
[[9, "C"], [8, "H"], [9, "S"], [4, "C"], [7, "D"]]
]
game = Poker.new(hands)
p game.high_cards # => [[[10, "D"], [9, "D"], [8, "C"], [7, "C"], [3, "C"]]]
This "works," but I certainly agree with Amadan that it's a hack. An interesting and instructive one, maybe, but a hack all the same. TIA for any suggestions.
回答1:
I am not sure why all this talk of enums.
I'll presume hands are in a format similar to this:
hands = [Hand.new([[12, "H"], [10, "H"], [8, "D"], [3, "D"], [2, "C"]]),
Hand.new([[10, "D"], [9, "H"], [3, "C"], [2, "D"], [2, "H"]]),
Hand.new([[12, "D"], [10, "S"], [8, "C"], [3, "S"], [2, "H"]]),
Hand.new([[12, "C"], [9, "S"], [8, "C"], [8, "S"], [8, "S"]])]
and you want to get the 0th and 2nd element. I'll also assume the hands are sorted, per your assert. As far as I can see, this is all that is needed, since arrays compare lexicographically:
max_ranks = hands.map { |hand| hand.cards.map(&:first) }.max
max_hands = hands.select { |hand| hand.cards.map(&:first) == max_ranks }
Alternately, use group_by
(a bit better, as it doesn't need to calculate ranks twice):
hands_by_ranks = hands.group_by { |hand| hand.cards.map(&:first) }
max_hands = hands_by_ranks[hands_by_ranks.keys.max]
回答2:
As already mentioned in the comments, there is no way to reliably retrieve the underlying object, because the enumerator doesn't always have one.
While your solution will work for this specific case, I would suggest coming up with another approach that will not depend on the implementation details of the enumerator object.
One of the possible solutions would be to pass an instance of Hand
along with the enumerator.
class Poker
def high_cards(hands)
hand_enums = hands.map { |hand| [hand, hand.to_enum] }
loop do
max_rank = hand_enums.map(&:last).map(&:peek).max
hand_enums.delete_if {|enum| enum.last.peek != max_rank }
hand_enums.each {|pair| pair.last.next}
end
hand_enums.map(&:first)
end
end
Another more object-oriented approach is to introduce a custom Enumerator
and expose the underlying object in a more explicit way:
class HandEnumerator < Enumerator
attr_reader :hand
def initialize(hand, &block)
@hand = hand
super(block)
end
end
class Hand
def to_enum
HandEnumerator.new(self) { |yielder| @cards.each { |card| yielder << card.first }}
end
# To satisfy interface of enumerator creation
def enum_for
to_enum
end
end
class Poker
def high_cards(hands)
hand_enums = hands.map(&:to_enum)
loop do
max_rank = hand_enums.map(&:peek).max
hand_enums.delete_if {|enum| enum.peek != max_rank }
hand_enums.each(&:next)
end
hand_enums.map(&:hand)
end
end
来源:https://stackoverflow.com/questions/60073714/ruby-to-enum-whats-the-best-way-to-extract-the-original-object-from-the-enume