Поток управления
Julia предоставляет различные конструкции для управления потоком:
-
Составные выражения:
beginи;. -
Условное выполнение:
if-elseif-elseи?:(тернарный оператор). -
Короткое замыкание: логические операторы
&&(«и») и||(«или»), а также цепочные сравнения. -
Повторяющееся выполнение: циклы:
whileиfor. -
Обработка исключений:
try-catch,errorиthrow. -
Задачи (иначе корутины):
yieldto.
Первые пять механизмов управления потоком являются стандартными для языков высокого уровня программирования. Task не являются такими стандартными: они обеспечивают нелокальный поток управления, позволяя переключаться между временно приостановленными вычислениями. Это мощная конструкция: как обработка исключений, так и кооперативное многозадачность реализуются в Julia с помощью задач. Для повседневной программирования прямой работы с задачами не требуется, но некоторые задачи решаются намного проще с их использованием.
Составные выражения
Иногда удобно иметь одно выражение, которое вычисляет несколько подвыражений в определённом порядке, возвращая значение последнего подвыражения в качестве своего значения. Существуют две конструкции Julia, которые выполняют эту задачу: begin блоки и ; цепочки. Значение обеих конструкций составных выражений — это значение последнего подвыражения. Вот пример блока begin:
julia> z = begin
x = 1
y = 2
x + y
end
3
Поскольку это довольно маленькие, простые выражения, их можно легко разместить на одной строке, где синтаксис ; цепочки пригодится:
julia> z = (x = 1; y = 2; x + y) 3
Этот синтаксис особенно полезен с краткой формой определения функций на одной строке, представленной в Функции. Хотя это типично, нет требования, чтобы begin блоки были многострочными или чтобы ; цепочки были однострочными:
julia> begin x = 1; y = 2; x + y end
3
julia> (x = 1;
y = 2;
x + y)
3
Условное выполнение
Условное выполнение позволяет выполнять или не выполнять части кода в зависимости от значения булевого выражения. Вот анатомия синтаксиса условного оператора if-elseif-else:
if x < y
println("x is less than y")
elseif x > y
println("x is greater than y")
else
println("x is equal to y")
end
Если выражение условия x < y имеет значение true, то соответствующий блок выполняется; в противном случае выражение условия x > y вычисляется, и если оно имеет значение true, то соответствующий блок выполняется; если ни одно выражение не истинно, выполняется блок else. Вот как это работает на практике:
julia> function test(x, y)
if x < y
println("x is less than y")
elseif x > y
println("x is greater than y")
else
println("x is equal to y")
end
end
test (generic function with 1 method)
julia> test(1, 2)
x is less than y
julia> test(2, 1)
x is greater than y
julia> test(1, 1)
x is equal to y
Блоки elseif и else необязательны, и можно использовать любое количество блоков elseif. Выражения условия в конструкции if-elseif-else вычисляются до тех пор, пока первое из них не примет значение true, после чего связанный блок выполняется, и дальнейшие выражения условия или блоки не выполняются.
Блоки if «прозрачные», т. е. они не вводят локальную область видимости. Это означает, что новые переменные, определённые внутри пунктов if, могут быть использованы после блока if, даже если они не были определены ранее. Таким образом, мы могли бы определить функцию test выше как
julia> function test(x,y)
if x < y
relation = "less than"
elseif x == y
relation = "equal to"
else
relation = "greater than"
end
println("x is ", relation, " y.")
end
test (generic function with 1 method)
julia> test(2, 1)
x is greater than y.
Переменная relation объявлена внутри блока if, но используется вне его. Однако, если вы полагаетесь на такое поведение, убедитесь, что все возможные пути кода определяют значение переменной. Следующее изменение в функции выше приводит к ошибке во время выполнения
julia> function test(x,y)
if x < y
relation = "less than"
elseif x == y
relation = "equal to"
end
println("x is ", relation, " y.")
end
test (generic function with 1 method)
julia> test(1,2)
x is less than y.
julia> test(2,1)
ERROR: UndefVarError: `relation` not defined in local scope
Stacktrace:
[1] test(::Int64, ::Int64) at ./none:7
Блоки if также возвращают значение, что может показаться неинтуитивно для пользователей, знакомых с другими языками программирования. Это значение просто возвращаемое значение последнего выполненного оператора в выбранном ветвлении, так что
julia> x = 3
3
julia> if x > 0
"positive!"
else
"negative..."
end
"positive!"
Обратите внимание, что очень короткие условные операторы (однострочные) часто записываются с использованием короткого замыкания в Julia, как описано в следующем разделе.
В отличие от C, MATLAB, Perl, Python и Ruby – но как Java и ещё несколько других строгих, типизированных языков – это ошибка, если значение выражения условия — что-либо помимо true или false:
julia> if 1
println("true")
end
ERROR: TypeError: non-boolean (Int64) used in boolean context
Эта ошибка указывает на то, что условное выражение было не того типа: Int64 вместо требуемого Bool.
Так называемый «тернарный оператор», ?:, тесно связан с синтаксисом if-elseif-else, но используется там, где требуется условный выбор между значениями одного выражения, а не условное выполнение более длинных блоков кода. Он получил своё название от того, что является единственным оператором в большинстве языков, принимающим три операнда:
a ? b : c
Выражение a до ? — это выражение условия, и тернарная операция вычисляет выражение b до :, если условие a имеет значение true, или выражение c после :, если оно равно false. Обратите внимание, что пробелы вокруг ? и : обязательны: выражение типа a?b:c не является допустимым тернарным выражением (но новая строка допустима после ? и :).
Самый простой способ понять это поведение — посмотреть пример. В предыдущем примере вызов println общий для всех трёх ветвей: единственный реальный выбор — это какая строковая литерал вывести. Это можно записать более кратко с помощью тернарного оператора. Для ясности, давайте сначала рассмотрим двустороннюю версию:
julia> x = 1; y = 2; julia> println(x < y ? "less than" : "not less than") less than julia> x = 1; y = 0; julia> println(x < y ? "less than" : "not less than") not less than
Если выражение x < y истинно, то всё выражение тернарного оператора оценивается как строка "less than", иначе оно оценивается как строка "not less than". Исходный трёхсторонний пример требует цепочки нескольких применений тернарного оператора:
julia> test(x, y) = println(x < y ? "x is less than y" :
x > y ? "x is greater than y" : "x is equal to y")
test (generic function with 1 method)
julia> test(1, 2)
x is less than y
julia> test(2, 1)
x is greater than y
julia> test(1, 1)
x is equal to y
Для облегчения цепочки оператор ассоциируется справа налево.
Важно, что, подобно if-elseif-else, выражения до и после : оцениваются только в том случае, если выражение условия имеет значение true или false соответственно:
julia> v(x) = (println(x); x)
v (generic function with 1 method)
julia> 1 < 2 ? v("yes") : v("no")
yes
"yes"
julia> 1 > 2 ? v("yes") : v("no")
no
"no"
Короткое замыкание
Операторы && и || в Julia соответствуют логическим операциям «и» и «или» соответственно и обычно используются для этой цели. Однако они обладают дополнительным свойством короткого замыкания: они не обязательно вычисляют свой второй аргумент, как объяснено ниже. (Также существуют битовые & и | операторы, которые могут использоваться в качестве логических «и» и «или» без короткого замыкания, но имейте в виду, что & и | имеют более высокий приоритет, чем && и || для порядка вычислений.)
Короткое замыкание очень похоже на условное выполнение. Это поведение существует в большинстве языков императивного программирования, имеющих булевы операторы && и ||: в последовательности булевых выражений, соединённых этими операторами, вычисляется только минимальное необходимое количество выражений для определения окончательного булевого значения всей цепочки. Некоторые языки (например, Python) называют их and (&&) и or (||). Конкретно это означает, что:
- В выражении
a && bподвыражениеbоценивается только тогда, когдаaоценивается какtrue. - В выражении
a || bподвыражениеbоценивается только тогда, когдаaоценивается какfalse.
Поскольку a && b должно быть false, если a равно false, независимо от значения b, и аналогично, значение a || b должно быть истинным, если a истинно, независимо от значения b. И &&, и || ассоциируются справа налево, но && имеет более высокий приоритет, чем ||. Экспериментировать с этим поведением очень просто:
julia> t(x) = (println(x); true) t (generic function with 1 method) julia> f(x) = (println(x); false) f (generic function with 1 method) julia> t(1) && t(2) 1 2 true julia> t(1) && f(2) 1 2 false julia> f(1) && t(2) 1 false julia> f(1) && f(2) 1 false julia> t(1) || t(2) 1 true julia> t(1) || f(2) 1 true julia> f(1) || t(2) 1 2 true julia> f(1) || f(2) 1 2 false
Аналогичным образом можно легко поэкспериментировать с ассоциативностью и приоритетом различных комбинаций операторов && и ||.
Это поведение часто используется в Julia для создания альтернативы очень коротким if операторам. Вместо if <cond> <statement> end можно написать <cond> && <statement> (что можно прочитать как: <условие> и затем <утверждение>). Аналогично, вместо if ! <cond> <statement> end можно написать <cond> || <statement> (что можно прочитать как: <условие> или иначе <утверждение>).
Например, рекурсивную функцию факториала можно определить так:
julia> function fact(n::Int)
n >= 0 || error("n must be non-negative")
n == 0 && return 1
n * fact(n-1)
end
fact (generic function with 1 method)
julia> fact(5)
120
julia> fact(0)
1
julia> fact(-1)
ERROR: n must be non-negative
Stacktrace:
[1] error at ./error.jl:33 [inlined]
[2] fact(::Int64) at ./none:2
[3] top-level scope
Булевы операции без короткого замыкания можно выполнить с помощью битовых булевых операторов, представленных в Математические операции и элементарные функции: & и |. Это обычные функции, которые поддерживают инфиксный синтаксис операторов, но всегда вычисляют свои аргументы:
julia> f(1) & t(2) 1 2 false julia> t(1) | t(2) 1 2 true
Так же, как выражения условия, используемые в if, elseif или операторе тернарного условия, операнды && или || должны быть булевыми значениями (true или false). Использование небулевого значения в любом месте, кроме последней записи в цепочке условных выражений, является ошибкой:
julia> 1 && true ERROR: TypeError: non-boolean (Int64) used in boolean context
С другой стороны, в конце цепочки условных выражений можно использовать любой тип выражения. Оно будет вычислено и возвращено в зависимости от предшествующих условных выражений:
julia> true && (x = (1, 2, 3)) (1, 2, 3) julia> false && (x = (1, 2, 3)) false
Повторное вычисление: циклы
Существуют два конструкта для повторного вычисления выражений: цикл while и цикл for. Вот пример цикла while:
julia> i = 1;
julia> while i <= 3
println(i)
global i += 1
end
1
2
3
Цикл while вычисляет выражение условия (i <= 3 в этом случае) и, пока оно остается true, продолжает вычислять тело цикла while. Если выражение условия является false при первом достижении цикла while, тело никогда не будет вычислено.
Цикл for упрощает написание общих выражений повторного вычисления. Поскольку подсчет вверх и вниз, как в приведенном выше цикле while, очень распространен, он может быть выражен более лаконично с помощью цикла for:
julia> for i = 1:3
println(i)
end
1
2
3
Здесь 1:3 — это объект range, представляющий последовательность чисел 1, 2, 3. Цикл for проходит по этим значениям, присваивая каждое из них по очереди переменной i. В общем, конструкция for может циклироваться по любому объекту "итерируемому" (или "контейнеру"), от диапазона, например, 1:3 или 1:3:13 (a StepRange обозначающий каждый третий целое число 1, 4, 7, …, 13) до более общих контейнеров, таких как массивы, включая итераторы, определенные кодом пользователя или внешние пакеты. Для контейнеров, отличных от диапазонов, альтернативный (но полностью эквивалентный) ключевой запрос in или ∈ обычно используется вместо =, так как это делает код более понятным:
julia> for i in [1,4,0]
println(i)
end
1
4
0
julia> for s ∈ ["foo","bar","baz"]
println(s)
end
foo
bar
baz
Различные типы итерируемых контейнеров будут представлены и обсуждаться в последующих разделах руководства (см., например, Многомерные массивы).
Одно довольно важное различие между предыдущей формой цикла while и формой цикла for — это область видимости переменной. Цикл for всегда вводит новую переменную итерации в своем теле, независимо от того, существует ли переменная с таким же именем в объемлющей области видимости. Это подразумевает, что, с одной стороны, переменная i не должна быть объявлена перед циклом. С другой стороны, она не будет видна за пределами цикла, и внешняя переменная с таким же именем не будет затронута. Для проверки этого вам понадобится либо новый экземпляр интерактивной сессии, либо другое имя переменной:
julia> for j = 1:3
println(j)
end
1
2
3
julia> j
ERROR: UndefVarError: `j` not defined in `Main`
julia> j = 0;
julia> for j = 1:3
println(j)
end
1
2
3
julia> j
0
Используйте for outer, чтобы изменить последнее поведение и повторно использовать существующую локальную переменную.
См. Область видимости переменных для подробного объяснения области видимости переменных, outer, и как она работает в Julia.
Иногда удобно завершать повторение цикла while, прежде чем условие проверки станет ложным, или останавливать итерацию в цикле for, прежде чем будет достигнут конец итерируемого объекта. Это можно сделать с помощью ключевого слова break:
julia> i = 1;
julia> while true
println(i)
if i >= 3
break
end
global i += 1
end
1
2
3
julia> for j = 1:1000
println(j)
if j >= 3
break
end
end
1
2
3
Без ключевого слова break, приведенный выше цикл while никогда не завершится самостоятельно, а цикл for будет итерироваться до 1000. Эти циклы оба завершаются преждевременно с помощью break.
В других обстоятельствах полезно иметь возможность остановить итерацию и немедленно перейти к следующей. Это достигается с помощью ключевого слова continue:
julia> for i = 1:10
if i % 3 != 0
continue
end
println(i)
end
3
6
9
Это несколько искусственный пример, так как мы могли бы получить то же поведение более наглядно, отрицая условие и поместив вызов println внутри блока if. В реальном использовании после continue есть больше кода, и часто существуют несколько точек, из которых осуществляется вызов continue.
Вложенные циклы for можно объединить в один внешний цикл, сформировав декартово произведение своих итерируемых объектов:
julia> for i = 1:2, j = 3:4
println((i, j))
end
(1, 3)
(1, 4)
(2, 3)
(2, 4)
С этим синтаксисом итерируемые объекты по-прежнему могут ссылаться на переменные внешнего цикла; например, for i = 1:n, j = 1:i допустимо. Однако оператор break внутри такого цикла завершает всю вложенность циклов, а не только внутренний. Обе переменные (i и j) устанавливаются на текущие значения итерации каждый раз, когда выполняется внутренний цикл. Поэтому присвоения i не будут видны последующим итерациям:
julia> for i = 1:2, j = 3:4
println((i, j))
i = 0
end
(1, 3)
(1, 4)
(2, 3)
(2, 4)
Если бы этот пример был переписан с использованием ключевого слова for для каждой переменной, то вывод был бы другим: второе и четвертое значения содержали бы 0.
Несколько контейнеров можно перебирать одновременно в одном цикле for, используя zip:
julia> for (j, k) in zip([1 2 3], [4 5 6 7])
println((j,k))
end
(1, 4)
(2, 5)
(3, 6)
Использование zip создаст итератор, являющийся кортежем, содержащим под-итераторы для переданных ему контейнеров. Итератор zip будет перебирать все под-итераторы в порядке, выбирая $i$-й элемент каждого под-итератора в $i$-й итерации цикла for. Как только какой-либо из под-итераторов исчерпается, цикл for остановится.
Обработка исключений
Когда происходит непредвиденное условие, функция может не быть в состоянии вернуть разумное значение своему вызывающему объекту. В таких случаях, возможно, лучше всего, чтобы исключительное условие либо завершало программу, выведя сообщение об ошибке с диагностикой, либо, если программист предоставил код для обработки таких исключительных ситуаций, позволить этому коду принять соответствующие действия.
Встроенные Exception
Exception выбрасываются, когда произошло непредвиденное условие. Встроенные Exception, перечисленные ниже, все прерывают обычное управление потоком.
Например, функция sqrt выбрасывает DomainError, если применяется к отрицательному действительному значению:
julia> sqrt(-1) ERROR: DomainError with -1.0: sqrt was called with a negative real argument but will only return a complex result if called with a complex argument. Try sqrt(Complex(x)). Stacktrace: [...]
Вы можете определить свои собственные исключения следующим образом:
julia> struct MyCustomException <: Exception end
Функция throw
Исключения могут быть созданы явно с помощью throw. Например, функция, определенная только для неотрицательных чисел, могла быть написана для throw DomainError, если аргумент отрицательный:
julia> f(x) = x>=0 ? exp(-x) : throw(DomainError(x, "argument must be non-negative")) f (generic function with 1 method) julia> f(1) 0.36787944117144233 julia> f(-1) ERROR: DomainError with -1: argument must be non-negative Stacktrace: [1] f(::Int64) at ./none:1
Обратите внимание, что DomainError без скобок не является исключением, а типом исключения. Для получения объекта Exception его необходимо вызвать:
julia> typeof(DomainError(nothing)) <: Exception true julia> typeof(DomainError) <: Exception false
Кроме того, некоторые типы исключений принимают один или несколько аргументов, которые используются для отчётности об ошибке:
julia> throw(UndefVarError(:x)) ERROR: UndefVarError: `x` not defined
Этот механизм можно легко реализовать с помощью пользовательских типов исключений, следуя тому, как написано UndefVarError:
julia> struct MyUndefVarError <: Exception
var::Symbol
end
julia> Base.showerror(io::IO, e::MyUndefVarError) = print(io, e.var, " not defined")
При написании сообщения об ошибке предпочтительно использовать строчную букву в первом слове. Например,
size(A) == size(B) || throw(DimensionMismatch("size of A not equal to size of B"))
предпочтительнее, чем
size(A) == size(B) || throw(DimensionMismatch("Size of A not equal to size of B")).
Однако иногда имеет смысл сохранить заглавную первую букву, например, если аргумент функции — заглавная буква:
size(A,1) == size(B,2) || throw(DimensionMismatch("A has first dimension...")).
Ошибки
Функция error используется для создания ErrorException, который прерывает нормальный поток управления.
Предположим, что мы хотим немедленно остановить выполнение, если извлечь квадратный корень из отрицательного числа. Для этого мы можем определить более строгую версию функции sqrt, которая поднимает ошибку, если её аргумент отрицательный:
julia> fussy_sqrt(x) = x >= 0 ? sqrt(x) : error("negative x not allowed")
fussy_sqrt (generic function with 1 method)
julia> fussy_sqrt(2)
1.4142135623730951
julia> fussy_sqrt(-1)
ERROR: negative x not allowed
Stacktrace:
[1] error at ./error.jl:33 [inlined]
[2] fussy_sqrt(::Int64) at ./none:1
[3] top-level scope
Если fussy_sqrt вызывается с отрицательным значением из другой функции, вместо попытки продолжить выполнение вызывающей функции, она немедленно возвращается, отображая сообщение об ошибке в интерактивной сессии:
julia> function verbose_fussy_sqrt(x)
println("before fussy_sqrt")
r = fussy_sqrt(x)
println("after fussy_sqrt")
return r
end
verbose_fussy_sqrt (generic function with 1 method)
julia> verbose_fussy_sqrt(2)
before fussy_sqrt
after fussy_sqrt
1.4142135623730951
julia> verbose_fussy_sqrt(-1)
before fussy_sqrt
ERROR: negative x not allowed
Stacktrace:
[1] error at ./error.jl:33 [inlined]
[2] fussy_sqrt at ./none:1 [inlined]
[3] verbose_fussy_sqrt(::Int64) at ./none:3
[4] top-level scope
Блок try/catch
Блок try/catch позволяет проверять Exception и элегантно обрабатывать ситуации, которые обычно могут сломать ваше приложение. Например, в приведенном ниже коде функция для вычисления квадратного корня обычно генерирует исключение. Разместив блок try/catch вокруг него, мы можем смягчить это здесь. Вы можете выбрать, как вы хотите обработать это исключение, например, зарегистрировать его, вернуть плейсхолдер или, как в случае ниже, просто напечатать сообщение. Важно учитывать при решении, как обрабатывать непредвиденные ситуации, что использование блока try/catch значительно медленнее, чем использование условных переходов для обработки таких ситуаций. Ниже приведены дополнительные примеры обработки исключений с помощью блока try/catch:
julia> try
sqrt("ten")
catch e
println("You should have entered a numeric value")
end
You should have entered a numeric value
Блоки try/catch также позволяют сохранить Exception в переменной. В следующем искусственном примере вычисляется квадратный корень из второго элемента x, если x индексируемый, в противном случае предполагается, что x — действительное число, и возвращается его квадратный корень:
julia> sqrt_second(x) = try
sqrt(x[2])
catch y
if isa(y, DomainError)
sqrt(complex(x[2], 0))
elseif isa(y, BoundsError)
sqrt(x)
end
end
sqrt_second (generic function with 1 method)
julia> sqrt_second([1 4])
2.0
julia> sqrt_second([1 -4])
0.0 + 2.0im
julia> sqrt_second(9)
3.0
julia> sqrt_second(-9)
ERROR: DomainError with -9.0:
sqrt was called with a negative real argument but will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
[...]
Обратите внимание, что символ, следующий за catch, всегда интерпретируется как имя исключения, поэтому при написании выражений try/catch в одной строке нужна осторожность. Следующий код не будет работать, чтобы вернуть значение x в случае ошибки:
try bad() catch x end
Вместо этого используйте точку с запятой или вставьте перенос строки после catch:
try bad() catch; x end
try bad()
catch
x
end
Мощность конструкции try/catch заключается в возможности немедленно развернуть глубоко вложенное вычисление до значительно более высокого уровня в стеке вызывающих функций. Существуют ситуации, когда ошибка не произошла, но возможность развернуть стек и передать значение на более высокий уровень желательна. Julia предоставляет функции rethrow, backtrace, catch_backtrace и current_exceptions для более продвинутой обработки ошибок.
Блоки else
Эта функциональность требует как минимум Julia 1.8.
В некоторых случаях может потребоваться не только правильно обработать случай ошибки, но и выполнить некоторый код только если блок try выполнится успешно. Для этого после блока else можно указать блок catch, который выполняется всякий раз, когда ранее не было выброшено никаких ошибок. Преимущество перед включением этого кода в блок try заключается в том, что любые дальнейшие ошибки не будут безмолвно перехвачены блоком catch.
local x
try
x = read("file", String)
catch
# handle read errors
else
# do something with x
end
Блоки try, catch, else и finally каждый вводят свои блоки области видимости, поэтому, если переменная определена только в блоке try, к ней нельзя получить доступ из блока else или finally:
julia> try
foo = 1
catch
else
foo
end
ERROR: UndefVarError: `foo` not defined in `Main`
Suggestion: check for spelling errors or missing imports.
Используйте ключевое слово local вне блока try, чтобы сделать переменную доступной из любой точки внешней области видимости.
Блоки finally
В коде, выполняющем изменения состояния или использующем ресурсы, такие как файлы, обычно требуется выполнение работы по очистке (например, закрытие файлов) при завершении выполнения кода. Исключения потенциально усложняют эту задачу, так как могут привести к выходу из блока кода до его нормального завершения. Ключевое слово finally предоставляет способ выполнения некоторого кода при выходе из заданного блока кода, независимо от того, как он вышел.
Например, вот как мы можем гарантировать, что открытый файл закрыт:
f = open("file")
try
# operate on file f
finally
close(f)
end
Когда управление покидает блок try (например, из-за return или просто нормального завершения), close(f) будет выполнен. Если блок try завершается из-за исключения, исключение будет продолжать распространяться. Блок catch может быть объединен с блоками try и finally. В этом случае блок finally будет выполнен после того, как блок catch обработает ошибку.
Задачи (также Потоки)
Задачи — это элемент управления потоком, который позволяет приостанавливать и возобновлять вычисления гибким способом. Мы упоминаем их только для полноты; для полного обсуждения см. Асинхронное программирование.
© 2009–2024 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.11/manual/control-flow/