Просто для удовольствия - могу ли я написать собственный NilClass для конкретного использования?

РЕДАКТИРОВАТЬ | Или другой вопрос, на эту же тему предмета. Могу ли я написать свое собственное определение класса, которое заставит работать все следующее?

o = WeirdObject.new
puts "Object o evaluates as true in boolean expressions" if o # Line never output
puts "Object o is nil?" if o.nil? # Line is output

Я могу сделать nil? все просто, так как это всего лишь метод. Но не знаю, как сделать так, чтобы оно оценивалось как неверное значение в логическом выражении (включая только самое основное выражение "o").

Оригинальный вопрос следует...

Еще больше из любопытства о том, сколько удовольствия я могу получить с Ruby (1.9.2) на самом деле...

Когда пользователь не вошел в систему, я хотел бы иметь возможность обнаружить его с unless @user или если @user.nil? и т.д., но в то же время я хотел бы иметь возможность вызывать методы User на этом объекте и пусть они возвращают то же самое NilClass (ala Objective-C), так как в большинстве мест это устранит необходимость в шаблоне if @user код, пока еще действует как nil в логических выражениях.

Я вижу, что вы не можете подкласс NilClass, так как у него нет #new метод. Вы можете добавлять методы непосредственно к экземпляру, но это одноэлементное приложение, поэтому это могло бы вызвать проблемы, если бы единственное место, где я хотел наблюдать это поведение, было с незарегистрированными пользователями.

Является ли это возможным?

Я имею в виду, что-то вроде этого:

class CallableNil < NilClass
  def method_missing(meth, *args, &block)
    self
  end
end

user = CallableNil.new # or .singleton, or something

puts "Got here" unless user # Outputs
puts "And here" unless user.nil? # also outputs

puts user.username # no-op, returns nil

Это в основном то, как Objective-C обрабатывает ноль, и это полезно в некоторых обстоятельствах.

Я думаю, я не мог бы создать подкласс NilClass и просто реализовать методы, которые заставили бы его быть ложным в логических выражениях?

РЕДАКТИРОВАТЬ | Ха-ха, ты действительно сильно разбиваешь Ruby, если переопределяешь верхний уровень NilClass... он вообще перестает отвечать на вызов любого метода.

>> class NilClass
>>   def method_missing(meth, *args, &block)
>>     self
>>   end
>> end
>> 
?> nil.foo
>> 
?> 
?> puts "here"
>> quit
>> # WTF? Close dammit!

4 ответа

Решение

Если вашим основным требованием является возможность вызова методов на nil значение без вызова исключения (или проверки того, что оно определено), ActiveSupport добавляет Object#try что дает вам возможность сделать это:

instance = Class.method_that_returns_nil

unless instance
  puts "Yes, we have no instance."
end

instance.try(:arbitrary_method)             # => nil
instance.try(:arbitrary_method, "argument") # => nil

Как вы уже видели NilClass имеет некоторые интересные побочные эффекты.

Другое потенциальное решение (не поточно-ориентированное):

def null!
  @@null_line = caller.first
end

class NilClass
  def method_missing(name, *args, &block)
    if caller.first == @@null_line
      nil
    else
      super
    end
  end
end


null!; nil.a.b.c.d.e # returns nil
nil.a # throws an error

Как правило, не рекомендуется менять nil объект таким образом - безотказный. Я бы порекомендовал использовать egonil или гем andand. Чтобы узнать больше о ссылках на эту тему [и для удовольствия;)], прочитайте этот пост в блоге.

Я искал очень похожую вещь сегодня, и общее мнение состоит в том, что "невозможно" превратить "ноль" в "ноль", не ломая все. Итак, вот как вы делаете невозможное:

class ::BasicObject
  def null!() self end
  def nil!() self end
end

class ::NilClass
  NULL_BEGIN = __LINE__
  def __mobj__caller()
    caller.find do |frame|
      (file, line) = frame.split(":")
      file != __FILE__ || !(NULL_BEGIN..NULL_END).cover?(line.to_i)
    end
  end
  def null?()
    @@null ||= nil
    @@null && @@null == __mobj__caller
  end
  def null!()
    @@null = __mobj__caller
    self
  end
  def nil!
    @@null = nil
    self
  end
  def method_missing(name, *args, &block)
    if null?
      self
    else
      nil!
      self
    end
  end
  NULL_END = __LINE__
end

(Простите форматирование, если оно облажалось, я делаю это на iPhone)

Это добавляет "ноль!" метод как ноль, так и всех объектов в целом. Для обычных объектов это не работает, и все работает как обычно (в том числе выдает исключения, когда методы отсутствуют). Для nil, однако, он не будет генерировать исключения, независимо от того, сколько методов в цепочке, и он все равно будет оцениваться точно как nil (так как это все еще обычный nil). Новое поведение ограничено только одной строкой, из которой "null!" назывался. После этого ноль возвращается к нормальному поведению.

if obj.null!.foo.bar.baz
    puts "normal execution if this is valid for obj"
end

obj = nil

if obj.null!.foo.bar.baz
    puts "this will not print, and no exception"
end

if obj.foo.bar.baz
    puts "this will throw an exception as usual"
end

Это достигается путем обхода дерева вызовов и маркировки первого файла / строки, который не является его собственным кодом, и использования его в качестве флага, чтобы указать, что он теперь находится в "нулевом" режиме. Пока файл / строка не изменятся, он будет продолжать действовать как "ноль". Как только он видит, что переводчик ушел, он возвращается к нормальному поведению.

Предостережения для этого подхода:

  • Ваше утверждение должно все помещаться на одной строке.

  • Ваше утверждение, вероятно, должно иметь фактический файл и номер строки (я не проверял его в IRB, но я подозреваю, что он не будет работать). Без этого он не может сказать, что он все еще выполняется в той же строке, поэтому он будет вероятно, вернется в нормальное состояние сразу же.

  • Новое поведение будет продолжать влиять на другие ноли до конца строки. Вы можете заставить его вести себя как обычно, позвонив "ноль!" позже в линии, хотя. Например:

    obj.null!.foo.bar.nil! || normal.nil.behavior

  • Все, что успешные вызовы методов делают в своих собственных телах, которые вызывают несуществующие методы против nil или вызывают "null!" вероятно, сбросит поведение, поскольку они имеют место при других номерах строк, но, теоретически, это не должно быть проблемой, если вы не делаете очень странные вещи.

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

https://github.com/gnovos/mobj

У меня есть еще один драгоценный камень, который может сделать это по-другому, если вы хотите более надежное решение (и не возражаете обернуть вещи в блоки):

require "ctx"

class NilClass
    ctx :null do
        def missing_method(*) self end
    end
end

И затем, где бы вы ни хотели, это поведение;

obj = nil

...

ctx :null do
    if obj.foo.bar
        puts "inside ctx block, acts like 'null!' from above, but extends to everything up and down the stack that inside of the ctx scope"
    end
end

if obj.foo.bar
    puts "outside ctx block, should be back to normal again"
end

Я не проверял этот последний, но это, вероятно, будет работать. Если вас это интересует, вы можете найти здесь ctx:

https://github.com/gnovos/ctx

Ура!

Другие вопросы по тегам