Ruby 3.1.2 как ссылаться на свойство объекта как параметр метода

Это может быть тривиально. Я хочу ссылаться на свойство объекта из списка параметров метода объекта, если это имеет смысл.

Тривиальный пример:

'tags'.ljust([calculated_length, obj.length].max)

где в этом примере obj относится к строке tags.

Возможно ли это вообще?

🤔 А знаете ли вы, что...
Ruby обладает сборщиком мусора для автоматического управления памятью.


50
4

Ответы:

Вы можете расширить класс String, добавив собственный метод:

class String
  def my_ljust(length)
    ljust([length, self.length].max)
  end
end

'tags'.my_ljust(8)

Но не уверен, что это имеет большой смысл, потому что метод ljust уже не сокращает длину строки.


Решено

ссылаться на свойство объекта из списка параметров метода объекта

Хотя вы находитесь внутри аргументов метода, вы все равно находитесь за пределами объекта. В этот момент Ruby не предоставляет ссылку на объект.

Если вы хотите сослаться на один и тот же объект несколько раз, обычно вы просто присваиваете его переменной:

str = 'tags'
str.ljust([8, str.length].max)
#=> "tags    "

Если вы не хотите этого делать, есть yield_self, который передает получателя данному блоку и возвращает результат блока:

'tags'.yield_self { |str| str.ljust([8, str.length].max) }
#=> "tags    "

Вы можете немного сократить это, ссылаясь на аргумент блока через _1:

'tags'.yield_self { _1.ljust([8, _1.length].max) }
#=> "tags    "

Хотя я согласен с @Stefan's Answer , особенно с той частью, которая не использует параметры пронумерованного блока, поскольку я думаю, что именованный параметр повышает читабельность и понимание, есть еще один вариант - использовать BasicObject#instance_eval

экземпляр_eval {|obj| блок } → объект

Оценивает строку, содержащую исходный код Ruby или заданный блок, в контексте получателя (obj). Чтобы установить контекст, переменной self присваивается значение obj во время выполнения кода, что дает коду доступ к переменным экземпляра obj и частным методам.

Итак, в этом случае ваш код можно изменить на:

'tags'.instance_eval { ljust([8, length].max) }
#=> "tags    "
# As the docs mention you can also pass a string
'tags'.instance_eval("ljust([8,length].max)")
#=> "tags    "

Поскольку контекст выполнения является получателем, неявным self будет 'tags', хотя этот объект также передается как параметр блока, поэтому вы можете получить ту же семантику, что и другой ответ.

'tags'.instance_eval { |str| str.ljust([8, str.length].max) }

Поскольку этот метод принимает блок, он также соответствует стандартам блоков, связанным с областью видимости переменных, поэтому следующие методы работают одинаково.

padding_length = 8 
'tags'.instance_eval { ljust([padding_length, length].max) }
#=> "tags    "

Обратите внимание, что из-за контекста выполнения доступны как переменные экземпляра, так и частные методы, и поэтому это следует учитывать при использовании этого метода.

В качестве альтернативы, с теми же оговорками, вы также можете использовать instance_exec. instance_exec обеспечивает дополнительное преимущество передачи аргументов для передачи в блок, например:

pad_me = ->(padding_length: 8) { ljust([padding_length, length].max) }

'tags'.instance_exec(&pad_me)
#=> "tags    "
'tags'.instance_exec(padding_length: 10, &pad_me)
#=> "tags      "

Почему изначально опубликованный код не работает

Стефан дал отличный практический ответ, но не объяснил, почему ваш текущий код не работает. Я попытаюсь сделать это, чтобы дополнить его ответ немного большим пониманием того, как работает Ruby. Используя ваш пример, но изменив object.length на obj.length, чтобы избежать путаницы с классом Object, вы приблизительно описали проблему:

# +obj+ refers to the string 'tags'
calculated_length = 2
'tags'.ljust([calculated_length, obj.length].max)

Это фактически вызывает объект Ruby Exception:

неопределенная локальная переменная или метод obj

Это происходит потому, что в Ruby почти все является методом и почти все является выражением, возвращающим значение. Поскольку вы не определили ничего для вызова метода String#length при вызове obj.length, это вызывает исключение NameError, поскольку obj на этом этапе оценки кода не определен. В зависимости от того, как вы определяете расчетную длину, у вас могут возникнуть те же проблемы, но текущее исключение возникает до того, как вы увидите эту ошибку.

Способы решения проблемы «неопределённой переменной»

В приведенном выше коде, если вы замените второй аргумент на String#ljust (т. е. obj.length) на определенную переменную или предварительно вычисленное целочисленное значение, то он будет работать без ошибок. Например:

# pass an Integer literal as your second argument
calculated_length = 2 + 1
"tags".ljust([calculated_length, 8].max)
#=> "tags    "

# give the :length method something defined as a receiver
calculated_length = 32 / 4
str = "tags"
str.ljust([calculated_length, str.length].max)
#=> "tags    "

Оба работают нормально. На самом деле это не проблема масштаба; просто когда вы используете строковый литерал, используемый вами список аргументов не имеет возможности определить, что исходный литерал предназначен быть получателем, когда вы вызываете String#length в своем списке аргументов.

Либо установив String как переменную, которая будет определена внутри списка аргументов, либо передав литерал в качестве аргумента блоку с помощью Kernel#yield_self (или его псевдонима Kernel#then), вы можете обойти эту проблему. фундаментальная проблема синтаксиса. например:

# pass your String literal into a block; technically,
# "tags" passes _itself_ into the block as positional
# block argument +_1+
"tags".then { _1.ljust([8, _1.length].max) }
#=> "tags    "

# assign your values to an instance or local variable
# so they won't be undefined inside your block or the
# argument lists
calculated_length = 2**3
str = "tags"

# using argument lists
str.ljust [calculated_length, str.length].max
#=> "tags    "

# block form using same values defined above
"tags".then { _1.ljust [calculated_length, str.length].max }
#=> "tags    "

Дальнейший рефакторинг: меньше элегантности, больше тестируемости

Кроме того, хотя это может показаться менее элегантным, вы можете рассмотреть возможность рефакторинга для удобства чтения и тестирования, а не для компактности. Читабельность, безусловно, зависит от наблюдателя, но тестируемость часто значительно повышается за счет наличия хорошо названных промежуточных и объясняющих переменных, которые можно проверять поэтапно. Например, лично я считаю, что следующий однострочный метод легче читать и отлаживать, чем текущее выражение Array, даже без документации YARD. Если хотите, есть и другие способы написать этот метод еще более явно.

# A one-line "endless method" tested with Ruby 3.4.4.
#
# @note The YARD documentation for this method and
#   its examples is 29 lines, longer than the single
#   line required to define it!
#
# The method name may seem oddly truncated, but makes
# more sense when you verbalize it as "custom padded
# string". Likewise, using more traditional signature
# like `custom_padded_str str` would be duplicative.
# Naming things in a way that communicates your
# authorial intent is always a hard problem!
#
# @example a String variable with default padding
#   str = "foo bar"
#   custom_padded str
#   #=> "foo bar "
# @example a String var with custom padding expression
#   str = "foo bar"
#   custom_padded str, 4**2
#    #=> "foo bar         "
# @example passing just String and Integer literals
#   custom_padded "string literal", 20
#   #=> "string literal      "
#
# @param str [String] required positional argument
# @param padding [Integer] a pre-calculated value
#   for padding the String; defaults to +8+
# @return [String] a left-justified padded string
def custom_padded(str, padding=8) = str.ljust(padding)

Я считаю, что это семантически более понятно и для него проще писать модульные тесты, но вы можете этого не делать. Моя основная причина рефакторинга такого типа заключается в том, что встраивание оцененных выражений в вызов String#ljust и особенно использование Array#max внутри вызова метода является частью того, что скрывает исходную ошибку, с которой вы столкнулись. В любом коде могут быть ошибки, но наличие пошагового кода, в котором значения и отдельные строки можно отлаживать с помощью Kernel#puts или debug gem, когда вы сталкиваетесь с проблемами, может избавить вас от многих душевных страданий.

Даже если вам не нравится мой конкретный подход к рефакторингу кода, перенос части вашей логики из вызова метода в объясняющие переменные, на мой взгляд, помог бы быстрее выявить основную причину. Ваш опыт работы со стилями кодирования, безусловно, может отличаться.