Початок, порятунок та забезпечення безпеки в Ruby?


547

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

Мені було цікаво, чи ensureбув еквівалент Ruby finallyв C #? Чи повинен я:

file = File.open("myFile.txt", "w")

begin
  file << "#{content} \n"
rescue
  #handle the error here
ensure
  file.close unless file.nil?
end

чи я повинен це робити?

#store the file
file = File.open("myFile.txt", "w")

begin
  file << "#{content} \n"
  file.close
rescue
  #handle the error here
ensure
  file.close unless file.nil?
end

Чи ensureдзвонять незважаючи ні на що, навіть якщо виняток не підвищується?


1
Ні добре. Як правило, працюючи із зовнішніми ресурсами, ви завжди хочете, щоб відкриття ресурсу` знаходилось всередині beginблоку.
Nowaker

Відповіді:


1181

Так, ensureгарантує, що код завжди оцінюється. Ось чому його називають ensure. Отже, він еквівалентний Java і C # finally.

Загальний потік begin/ rescue/ else/ ensure/ endвиглядає наступним чином :

begin
  # something which might raise an exception
rescue SomeExceptionClass => some_variable
  # code that deals with some exception
rescue SomeOtherException => some_other_variable
  # code that deals with some other exception
else
  # code that runs only if *no* exception was raised
ensure
  # ensure that this code always runs, no matter what
  # does not change the final value of the block
end

Ви можете залишити rescue, ensureабо else. Ви також можете залишити змінні, і в цьому випадку ви не зможете перевірити виняток у коді обробки винятків. (Ну, ви завжди можете використовувати глобальну змінну винятків, щоб отримати доступ до останнього винятку, який був піднятий, але це трохи хакітно.) І ви можете залишити клас винятку, і в цьому випадку будуть вилучені всі винятки, які успадковуються StandardError. (Будь ласка , зверніть увагу , що це не означає , що всі виключення спіймані, тому що є винятки , які є екземплярами , Exceptionале не StandardError. В основному дуже серйозні виключення , які ставлять під загрозу цілісність програми , такі як SystemStackError, NoMemoryError, SecurityError, NotImplementedError, LoadError, SyntaxError, ScriptError, Interrupt,SignalExceptionабо SystemExit.)

Деякі блоки утворюють неявні блоки виключень. Наприклад, визначення методів неявно також є блоками виключень, тому замість написання

def foo
  begin
    # ...
  rescue
    # ...
  end
end

ти пишеш просто

def foo
  # ...
rescue
  # ...
end

або

def foo
  # ...
ensure
  # ...
end

Те саме стосується classвизначень та moduleвизначень.

Однак у конкретному випадку, про який ви запитуєте, насправді є набагато краща ідіома. Взагалі, коли ви працюєте з деяким ресурсом, який вам потрібно очистити наприкінці, ви робите це, передаючи блок методу, який робить усе очищення для вас. Це схоже на usingблок в C #, за винятком того, що Ruby насправді досить потужний, що вам не доведеться чекати, коли первосвященики Майкрософт зійдуть з гори і люб'язно поміняють компілятор для вас. У Ruby ви можете просто реалізувати його самостійно:

# This is what you want to do:
File.open('myFile.txt', 'w') do |file|
  file.puts content
end

# And this is how you might implement it:
def File.open(filename, mode='r', perm=nil, opt=nil)
  yield filehandle = new(filename, mode, perm, opt)
ensure
  filehandle&.close
end

І що ви знаєте: це вже доступно в основній бібліотеці як File.open. Але це загальна модель, яку ви також можете використовувати у власному коді для здійснення будь-якого способу очищення ресурсів (à la usingв C #) або транзакцій або будь-якого іншого, про що ви могли б подумати.

Єдиний випадок, коли це не працює, якщо придбання та звільнення ресурсу розподіляються по різних частинах програми. Але якщо він локалізований, як у вашому прикладі, то ви можете легко використовувати ці блоки ресурсів.


BTW: в сучасному C # usingнасправді зайве, адже ви можете реалізувати блоки ресурсів у стилі Ruby самостійно:

class File
{
    static T open<T>(string filename, string mode, Func<File, T> block)
    {
        var handle = new File(filename, mode);
        try
        {
            return block(handle);
        }
        finally
        {
            handle.Dispose();
        }
    }
}

// Usage:

File.open("myFile.txt", "w", (file) =>
{
    file.WriteLine(contents);
});

81
Зауважте, що хоч ensureоператори виконуються останніми, вони не є поверненим значенням.
Кріс

30
Мені подобається бачити подібні внески на SO. Це вище, ніж те, що запитувала ОП, що стосується багатьох інших розробників, все ще залишається на темі. Я дізнався кілька речей з цієї відповіді + правки. Дякую за те, що ви не просто написали "Так, ensureдзвонять незважаючи ні на що".
Денніс

3
Зауважте, що гарантувати НЕ гарантовано. Візьміть випадок, коли у вас є початок / забезпечення / кінець всередині потоку, а потім ви викликаєте Thread.kill, коли викликається перший рядок блоку забезпечення. Це призведе до того, що решта забезпечення не буде виконана.
Тедді

5
@ Тедді: переконайтесь, що гарантовано розпочнеться виконання, не гарантується завершення. Ваш приклад - overkill - простий виняток всередині блоку забезпечення також призведе до його виходу.
Мартін Конечний

3
також зауважте, що немає гарантій, що забезпечуються вимоги. Я серйозно. Відключення живлення / помилка апаратури / зрив ОС може статися, і якщо ваше програмне забезпечення є критичним, це потрібно враховувати.
EdvardM

37

FYI, навіть якщо в rescueрозділі повторно підвищено виняток , ensureблок буде виконаний до того, як виконання коду продовжується до наступного обробника винятків. Наприклад:

begin
  raise "Error!!"
rescue
  puts "test1"
  raise # Reraise exception
ensure
  puts "Ensure block"
end

14

Якщо ви хочете переконатися, що файл закритий, слід скористатися формою блоку File.open:

File.open("myFile.txt", "w") do |file|
  begin
    file << "#{content} \n"
  rescue
  #handle the error here
  end
end

3
Я думаю, якщо ви не хочете обробляти помилку, а просто підвищити її та закрити ручку файлу, тут вам не потрібно починати рятування?
rogerdpack


5

Так, ensureЗАБЕЗПЕЧУЄТЬСЯ це запускається кожного разу, тому вам не потрібно file.closeв beginблоці.

До речі, хорошим способом тестування є:

begin
  # Raise an error here
  raise "Error!!"
rescue
  #handle the error here
ensure
  p "=========inside ensure block"
end

Ви можете перевірити, чи буде надруковано "========= всередині блоку забезпечення", коли є виняток. Тоді ви можете прокоментувати твердження, яке викликає помилку, і побачити, чи ensureвиконується твердження, побачивши, чи щось надрукується.


4

Ось чому нам потрібно ensure:

def hoge
  begin
    raise
  rescue  
    raise # raise again
  ensure  
    puts 'ensure' # will be executed
  end  
  puts 'end of func' # never be executed
end  

4

Так, ensureяк finally гарантії, що блок буде виконаний . Це дуже корисно для гарантування захисту критичних ресурсів, наприклад, закриття керування файлом із помилкою або випуску мютексу.


За винятком його випадку, жодної гарантії закриття файлу немає, оскільки File.openчастина НЕ знаходиться всередині блоку "start-secure". Тільки file.closeє, але цього недостатньо.
Новкер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.