08. Call stack. Изключения

08. Call stack. Изключения

08. Call stack. Изключения

7 ноември 2016

Днес

Преди това

SUGAR TIME!

Въпрос 1

Какви са съответните стойности тук и защо?

'text'.object_id                == 'text'.object_id        # => ???
'text'.freeze.object_id         == 'text'.object_id        # => ???
'text'.freeze.object_id         == 'text'.freeze.object_id # => ???
(foo = 'text').freeze.object_id == 'text'.freeze.object_id # => ???

Въпрос 1

Какви са съответните стойности тук и защо?

'text'.object_id                == 'text'.object_id        # => false
'text'.freeze.object_id         == 'text'.object_id        # => false
'text'.freeze.object_id         == 'text'.freeze.object_id # => true
(foo = 'text').freeze.object_id == 'text'.freeze.object_id # => false
  • Низовете в Ruby са mutable => всяко срещане на низ = нов обект
  • 'text'.freeze се оптимизира от Ruby и връща един и същи обект
  • Ако вече има такъв обект, с такова съдържание, той ще бъде преизползван
  • Важи само при първо дефиниране на низ с литерален синтаксис

Въпрос 2

Какво ще се случи при изпълнение на следния код (и защо):

case 42
  when String         then 'This is a string'
  when :odd?.to_proc  then 'This is an odd number'
  when :even?.to_proc then 'This is an even number'
  else                     'I have no f*cking idea what this is.'
end
  • Ще изведе "This is an even number"
  • case използва "оператора" ===, за да оценява различните случаи.
  • Всеки when се оценява така: condition === testable.
  • Proc имплементира === като извиква себе си с подадения на === аргумент.

Въпрос 3

Как създаваме анонимни класове? А модули?

  • anonymous_class = Class.new
  • anonymous_module = Module.new

Въпрос 4

Избройте поне 3 начина за дефиниране на класови методи.

Какви са конвенциите при използване на тези начини?

  • 1) def ClassName.method_name(); end - не се използва
  • 2) def self.method_name(); end - при дефиниране на един, максимум два класови метода
  • 3) class << self - при дефиниране на повече от един класов метод

Въпрос 5

Какво правят `send` и `public_send`? Каква е разликата между тях?

  • send може да извиква произволни методи на даден обект (дори private или protected)
  • public_send може да извиква само публични методи на даден обект

Call stack

Достъп до call стека

Пример за call stack

# inception.rb:
def roll_the_ball()         go_deep               end
def go_deep()               we_need_to_go_deeper  end
def we_need_to_go_deeper()  even_deeper_than_that end
def even_deeper_than_that() puts(caller)          end

roll_the_ball

Изпълняваме го с ruby inception.rb.

Пример за call stack - резултат

Примерът от предния слайд ще продуцира:

inception.rb:3:in `we_need_to_go_deeper'
inception.rb:2:in `go_deep'
inception.rb:1:in `roll_the_ball'
inception.rb:6:in `<main>' 

Call стекът обикновено е по-дълбок

(съкратен) пример от irb

> puts caller
.../irb/workspace.rb:86:in `eval'
.../irb/workspace.rb:86:in `evaluate'
.../irb/context.rb:380:in `evaluate'
.../irb.rb:492:in `block (2 levels) in eval_input'
.../irb.rb:624:in `signal_status'
.../irb.rb:489:in `block in eval_input'
.../irb/ruby-lex.rb:247:in `block (2 levels) in each_top_level_statement'
.../irb/ruby-lex.rb:233:in `loop'
.../irb/ruby-lex.rb:233:in `block in each_top_level_statement'
.../irb/ruby-lex.rb:232:in `catch'
.../irb/ruby-lex.rb:232:in `each_top_level_statement'
.../irb.rb:488:in `eval_input'
.../irb.rb:397:in `block in start'
.../irb.rb:396:in `catch'
.../irb.rb:396:in `start'
.../bin/irb:11:in `
' => nil

Call stacks и backtraces

Въпроси до тук?

Сега е моментът да ги зададете :-)

Обработка на грешки

Представете си, че пишете на C:

char source[1000000];
FILE* file = fopen("text-file.txt", "r");

if (file != NULL) {
    while ((symbol = getc(file)) != EOF) {
        strcat(source, &symbol);
    }
    fclose(file);
} else {
    // ERROR
}

Обработка на грешки

Представете си, че пишете качествен код на C:

char *source = NULL;
FILE *fp = fopen("foo.txt", "r");
if (fp != NULL) {
    // Go to the end of the file
    if (fseek(fp, 0L, SEEK_END) == 0) {
        // Get the size of the file
        long bufsize = ftell(fp);
        if (bufsize == -1) { /* ERROR */ }

        // Allocate our buffer to that size
        source = malloc(sizeof(char) * (bufsize + 1));
        if (source == NULL) { /* ERROR */ }

        // Go back to the start of the file
        if (fseek(fp, 0L, SEEK_SET) != 0) { /* ERROR */ }

        // Read the entire file into memory
        size_t newLen = fread(source, sizeof(char), bufsize, fp);

        if (ferror(fp) != 0) { /* ERROR */ }
        else { source[newLen++] = '\0'; }
    } else { /* ERROR */ }
} else { /* ERROR */ }

Обработка на грешки

Догади ли ви се?

NodeJS

Представете си, че пишете на NodeJS:

function liveADay(callback) {
  getUpInTheMorning(function(time) {
    brushYourTeeth(function() {
      goToFMI(function() {
        sleepInClass(function() {
          goHome(function() {
            liveADay(callback);
          });
        });
      });
    });
  });
}

NodeJS - error handling

function liveADay(callback) {
  getUpInTheMorning(function(err, time) {
    if (err) {
      callback(err); return; // err = 'too early in the morning'
    }

    goToFMI(function() {
      if (err) {
        callback(err); return; // err = {error: 'It is Saturday'}
      }

      sleepInClass(function(err) {
        if (err) {
          callback(err); return; // err = TeacherGivesSnickersError
        }

        goHome(function() {
          if (err) {
            callback(err); return; // err = {code: ENOENT, errno: -2}
          }

          liveADay(callback);
        });
      });
    });
  });
}

Summary

Обработка на грешки

Връщане на nil понякога е напълно приемливо. Например:

def find(collection, expected_element)
  collection.each do |element|
    return element if element == expected_element
  end

  nil
end

Изключения

Основни атрибути

Изключенията в Ruby са обекти като всичко останало – инстанции на клас Exception или негов наследник.

Имат три основни атрибута:

Основни атрибути (2)

Нека имаме инстанция на изключение в променлива error. Тогава:

Някои вградени изключения

foo               # NameError: undefined local variable or method `foo' for main:Object
1 / 0             # ZeroDivisionError: divided by 1
File.open         # ArgumentError: wrong number of arguments (0 for 1..3)
File.open('/Ah?') # Errno::ENOENT: No such file or directory @ rb_sysopen - /Ah?

Между другото, Errno::ENOENT си е нормално изключение:

Errno::ENOENT.ancestors.take_while { |kind| kind != Object }
# => [Errno::ENOENT, SystemCallError, StandardError, Exception]

Непълна йерархия

Object
+-- Exception
   +-- NoMemoryError
   +-- ScriptError
   |   +-- SyntaxError
   |   +-- LoadError
   +-- SecurityError
   +-- StandardError
       +-- ArgumentError
       +-- IndexError
       |   +-- KeyError
       |   +-- StopIteration
       +-- NameError
       |   +-- NoMethodError
       +-- RuntimeError
       +-- TypeError 

Наши собствени изключения

За да си направим клас-изключение, обикновено наследяваме от RuntimeError или StandardError:

class NoFriendsError < StandardError; end

Предизвикване на изключения

# Предизвиква RuntimeError
raise "'Prophet!' said I, 'Thing of evil!" # => error: RuntimeError

# Като горното, но с различен текст
raise RuntimeError, 'prophet still, if bird or devil!' # => error: RuntimeError

# Друг начин да предизвикаме RuntimeError
raise RuntimeError.new('Whether tempter sent, or whether...') # => error: RuntimeError

Хващане на изключения

begin
  puts '...tempest tossed thee here ashore'
  raise NameError, 'Desolate yet all undaunted'
rescue => ex
  ex.message   # => "Desolate yet all undaunted"
  ex.class     # => NameError
end

Хващане на изключения

хващане на конкретен тип

begin
  raise KeyError, 'on this desert land enchanted'
rescue ArgumentError => ex
  puts 'on this home by horror haunted'
rescue KeyError, TypeError => ex
  ex.message  # => "on this desert land enchanted"
  ex.class    # => KeyError
end

Какво хваща rescue?

rescue хваща "само" наследници на StandardError, ако не сме указали друго:

Object
+-- Exception
   +-- NoMemoryError
   +-- ScriptError
   +-- StandardError
       +-- ArgumentError
       +-- NameError
       |   +-- NoMethodError
       +-- RuntimeError 

Въпрос към вас

Какво ще се случи тук?

begin
  raise KeyError, 'tell me truly, I implore'
rescue IndexError => ex
  puts 'IndexError'
rescue KeyError => ex
  puts 'KeyError'
end

Ще се изведе 'IndexError'

Йерархията отново (непълна)

Object
+-- Exception
   +-- NoMemoryError
   +-- ScriptError
   |   +-- SyntaxError
   |   +-- LoadError
   +-- SecurityError
   +-- StandardError
       +-- ArgumentError
       +-- IndexError
       |   +-- KeyError
       |   +-- StopIteration
       +-- NameError
       |   +-- NoMethodError
       +-- RuntimeError
       +-- TypeError 

Хващане на изключения

приоритет на rescue клаузите

Припомняне KeyError < IndexError

$eh = 'foo'

begin
  raise KeyError, 'Is there - is there balm in Gilead?'
rescue IndexError => ex
  $eh = 'index'
rescue KeyError => ex
  $eh = 'key'
end

$eh    # => "index"

Изпълнява се първия rescue, за който изключението е kind_of? типа.

Запомнете

Динамичните езици обикновено ползват прости правила

Хващане на изключения

ensure клауза

Кодът в ensure клаузата се изпълнява винаги.

begin
  raise 'tell me - tell me, I implore!' if rand(2).zero?
ensure
  puts '????? ??? ?????, "?????????"'
end

Хващане на изключения

else клауза

else клаузата се изпълнява когато няма възникнало изключение.

begin
  launch_nukes
rescue
  puts 'Uh-oh! Something went wrong :('
else
  puts 'War... War never changes'
end

The beginning and the end

begin
  get_a_life
rescue NoFriendsError => ex
  puts 'Goodbye cruel world'
rescue InsufficientVespeneGasError, NotEnoughMineralsError
  puts 'show me the money'
rescue
  puts ';('
else
  puts 'Woohoo!'
ensure
  puts 'rm -rf ~/.history'
end

Въпроси до тук?

Сега е моментът да ги зададете :-)

rescue в метод

В случай, че ползвате rescue в метод така:

def execute
  begin
    potentially_dangerous
  rescue SomeException => e
    # Handle the error
  ensure
    # Ensure something always happens
  end
end

rescue в метод

Предпочитания вариант

По-добре е да го запишете без begin/end, което е еквивалентно на предното:

def execute
  potentially_dangerous
rescue SomeException => e
  # Handle the error
ensure
  # Ensure something always happens
end

Предизвикване на изключение

по време на обработка на друго

Ако възникне изключение при обработка на друго, старото се игнорира и се "вдига" новото.

begin
  raise KeyError
rescue
  raise TypeError
  puts "I'm a line of code, that's never executed ;("
end

raise в rescue

raise в rescue клауза "вдига" същото изключение, което се обработва.

begin
  raise KeyError, 'But high she shoots through air and light'
rescue
  puts 'Whoops'
  raise
end

begin/end

...е израз, като всичко друго в ruby

result = begin
  raise KeyError if rand(3).zero?
  raise NameError if rand(3).zero?
rescue KeyError
  'nyckel'
rescue NameError
  'namn'
else
  'ingenting'
end

result    # => "ingenting"

rescue като модификатор

[].fetch(1) rescue 'baba' # => "baba"

Exception#exception

raise type, message всъщност извиква type.exception(message), за да конструира изключение.

class Thing
  def exception(message)
    NameError.new(message)
  end
end

thing = Thing.new
raise thing, 'whoops' # => error: NameError

Въпроси до тук?

Сега е моментът да ги зададете :-)

Как да ползваме изключения

Може да разделим изключенията на два основни вида.

  1. Непредвидими грешки, причинени от "околната среда".
  2. Програмистки грешки, причинени от неправилна употреба на парче код.

Непредвидими грешки

Програмистки грешки

Какво да хващаме?

Изключения в библиотеки

It is recommended that a library should have one subclass of StandardError or RuntimeError and have specific exception types inherit from it. This allows the user to rescue a generic exception type to catch all exceptions the library may raise even if future versions of the library add new exception subclasses.

Изключения в библиотеки (2)

Въпроси до тук?

Сега е моментът да ги зададете :-)

catch и throw

catch и throw - общ вид

def go_deep
  go_deeper
end

def go_deeper
  throw :some_label, 'the value'
  raise 'This line WILL NOT execute.'
end

return_value = catch :some_label do
  go_deep
end

return_value == 'the value' # => true

catch и throw

накратко

catch и throw

edge cases

catch(:baba) do
  throw :baba, 'Penka'
  'Ginka'
end                       # => "Penka"

catch(:baba) { 'Ginka' }  # => "Ginka"

throw :baba, 'Penka'      # error: UncaughtThrowError

catch и throw

реален пример

Взет от Vagrant:

found = catch(:done) do
  [modified, added, removed].each do |changed|
    changed.each do |listenpath|
      throw :done, true if listenpath.start_with?(hostpath)
    end
  end

  # Make sure to return false if all else fails so that we
  # don't sync to this machine.
  false
end

catch и throw

реален пример - дали?

Какво ще кажете да го направим така?

found =
  [modified, added, removed].any? do |changed_paths|
    changed_paths.any? { |path| path.start_with? host_path }
  end

Или дори така:

changed_paths = [modified, added, removed].flatten

found = changed_paths.any? { |path| path.start_with? host_path }

catch и throw

(по-)реален пример

Взет от Discourse (и леко модифициран):

def get_excerpt(html)
  parser = Nokogiri::HTML::SAX::Parser.new(self)

  catch(:found_excerpt) do
    parser.parse(html)
  end
end

def some_parser_hooks
  # Make checks and gather info...
  # Found our thing!
  excerpt = 'something'
  throw :found_excerpt, excerpt
end

Въпроси