В программировании компьютера оператор присваивания устанавливает и/или сбрасывает значение, хранящееся в ячейке(ях) хранения, обозначенной именем переменной ; другими словами, он копирует значение в переменную. В большинстве императивных языков программирования оператор присваивания (или выражение) является фундаментальной конструкцией.
Сегодня наиболее часто используемая нотация для этой операции — (первоначально Superplan 1949–51, популяризированная Fortran 1957 и C ). Вторая наиболее часто используемая нотация — [1] (первоначально ALGOL 1958, популяризированная Pascal ). [2] Также используются многие другие нотации. В некоторых языках используемый символ рассматривается как оператор (что означает, что оператор присваивания в целом возвращает значение). Другие языки определяют присваивание как оператор (что означает, что его нельзя использовать в выражении).x = expr
x := expr
Присваивания обычно позволяют переменной удерживать разные значения в разное время в течение ее жизненного цикла и области действия . Однако некоторые языки (в первую очередь строго функциональные языки) не допускают такого рода «деструктивное» переназначение, поскольку это может подразумевать изменения нелокального состояния. Цель состоит в том, чтобы обеспечить ссылочную прозрачность , т. е. функции, которые не зависят от состояния некоторой переменной(ых), но производят те же результаты для заданного набора параметрических входов в любой момент времени. Современные программы на других языках также часто используют похожие стратегии, хотя и менее строгие, и только в определенных частях, чтобы уменьшить сложность, обычно в сочетании с дополнительными методологиями, такими как структурирование данных , структурное программирование и объектная ориентация .
Операция присваивания — это процесс в императивном программировании , в котором различные значения связываются с определенным именем переменной с течением времени. [1] Программа в такой модели работает, изменяя свое состояние с помощью последовательных операторов присваивания. [2] [3] Примитивы императивных языков программирования полагаются на присваивание для выполнения итерации . [4] На самом низком уровне присваивание реализуется с помощью машинных операций, таких как MOVE
или STORE
. [2] [4]
Переменные являются контейнерами для значений. Можно поместить значение в переменную и позже заменить его новым. Операция присваивания изменяет текущее состояние выполняемой программы. [3] Следовательно, присваивание зависит от концепции переменных . В присваивании:
expression
проводится в текущем состоянии программы.variable
вычисленное значение, заменяющее предыдущее значение этой переменной.Пример: Предположим, что a
это числовая переменная, тогда присваивание a := 2*a
означает, что содержимое переменной a
удваивается после выполнения оператора.
Пример фрагмента кода на языке C :
int x = 10 ; float y ; x = 23 ; y = 32.4f ;
В этом примере переменная x
сначала объявляется как int, а затем ей присваивается значение 10. Обратите внимание, что объявление и присваивание происходят в одном и том же операторе. Во второй строке y
объявляется без присваивания. В третьей строке x
переназначается значение 23. Наконец, y
присваивается значение 32.4.
Для операции присваивания необходимо, чтобы значение expression
было четко определено (это допустимое rvalue ) и чтобы variable
представляло изменяемую сущность (это допустимое изменяемое (неконстантное ) lvalue ) . В некоторых языках, как правило, динамических , не обязательно объявлять переменную перед присвоением ей значения. В таких языках переменная автоматически объявляется при первом ее назначении, при этом область ее действия, в которой она объявляется, варьируется в зависимости от языка.
Любое присваивание, которое изменяет существующее значение (например, x := x + 1
), запрещено в чисто функциональных языках. [4] В функциональном программировании присваивание не приветствуется в пользу одиночного присваивания, более известного как инициализация . Одиночное присваивание является примером связывания имен и отличается от присваивания, описанного в этой статье, тем, что его можно выполнить только один раз, обычно при создании переменной; последующее повторное присваивание не допускается.
Оценка выражения не имеет побочных эффектов , если она не изменяет наблюдаемое состояние машины, [5] кроме создания результата, и всегда создает одно и то же значение для одного и того же ввода. [4] Императивное присваивание может привносить побочные эффекты, уничтожая и делая старое значение недоступным при замене его новым, [6] и по этой причине называется деструктивным присваиванием в LISP и функциональном программировании , аналогично деструктивному обновлению .
Одиночное присваивание — единственная форма присваивания, доступная в чисто функциональных языках, таких как Haskell , которые не имеют переменных в смысле императивных языков программирования [4], а скорее именованных константных значений, возможно, составной природы, с их элементами, постепенно определяемыми по требованию , для ленивых языков. Чисто функциональные языки могут предоставить возможность для вычислений, выполняемых параллельно , избегая узкого места фон Неймана последовательного выполнения по одному шагу за раз, поскольку значения независимы друг от друга. [7]
Нечистые функциональные языки предоставляют как одиночное присваивание, так и истинное присваивание (хотя истинное присваивание обычно используется реже, чем в императивных языках программирования). Например, в Scheme как одиночное присваивание (с let
), так и истинное присваивание (с set!
) могут использоваться для всех переменных, а специализированные примитивы предоставляются для деструктивного обновления внутри списков, векторов, строк и т. д. В OCaml для переменных разрешено только одиночное присваивание через синтаксис; однако деструктивное обновление может использоваться для элементов массивов и строк с отдельным оператором, а также для полей записей и объектов, которые были явно объявлены программистом как изменяемые (то есть способные изменяться после их первоначального объявления).let name = value
<-
Функциональные языки программирования, которые используют одиночное присваивание, включают Clojure (для структур данных, а не vars), Erlang (он принимает множественное присваивание, если значения равны, в отличие от Haskell), F# , Haskell , JavaScript (для констант), Lava, OCaml , Oz (для переменных потока данных, а не ячеек), Racket (для некоторых структур данных, таких как списки, а не символы), SASL , Scala (для vals), SISAL , Standard ML . Код Prolog без возврата можно считать явным одиночным присваиванием, явным в том смысле, что его (именованные) переменные могут находиться в явно неназначенном состоянии или быть заданными ровно один раз. В Haskell, напротив, не может быть неназначенных переменных, и каждую переменную можно рассматривать как неявно заданную, когда она создается, ее значению (или, скорее, вычислительному объекту, который будет выдавать свое значение по требованию ).
В некоторых языках программирования оператор присваивания возвращает значение, а в других — нет.
В большинстве языков программирования, ориентированных на выражения (например, C ), оператор присваивания возвращает присвоенное значение, что позволяет использовать такие идиомы, как x = y = a
, в которых оператор присваивания y = a
возвращает значение a
, которое затем присваивается x
. В таком операторе, как , возвращаемое значение функции используется для управления циклом при присвоении того же значения переменной.while ((ch = getchar()) != EOF) {…}
В других языках программирования, например, Scheme , возвращаемое значение присваивания не определено, и такие идиомы недействительны.
В Haskell [8] нет присваивания переменных; но операции, подобные присваиванию (например , присваивание полю массива или полю изменяемой структуры данных), обычно оцениваются в типе единицы , который представлен как ()
. Этот тип имеет только одно возможное значение, поэтому не содержит никакой информации. Обычно это тип выражения, который оценивается исключительно по его побочным эффектам.
Некоторые шаблоны использования очень распространены, и поэтому часто имеют специальный синтаксис для их поддержки. Это в первую очередь синтаксический сахар для уменьшения избыточности в исходном коде, но также помогает читателям кода понять намерения программиста и дает компилятору подсказку для возможной оптимизации.
Случай, когда присваиваемое значение зависит от предыдущего, настолько распространен, что многие императивные языки, в частности C и большинство его потомков, предоставляют специальные операторы, называемые расширенным присваиванием , например *=
, поэтому a = 2*a
вместо этого его можно записать как a *= 2
. [3] Помимо синтаксического сахара, это помогает задаче компилятора, давая понять, что изменение переменной на месте a
возможно.
Оператор типа w = x = y = z
называется цепочечным присваиванием , в котором значение z
присваивается нескольким переменным w, x,
и y
. Цепочечные присваивания часто используются для инициализации нескольких переменных, как в
a = b = c = d = f = 0
Не все языки программирования поддерживают цепочечные присваивания. Цепочечные присваивания эквивалентны последовательности присваиваний, но стратегия оценки различается в разных языках. Для простых цепочечных присваиваний, таких как инициализация нескольких переменных, стратегия оценки не имеет значения, но если цели (l-значения) в присваивании связаны каким-либо образом, стратегия оценки влияет на результат.
В некоторых языках программирования ( например, C ) поддерживаются цепочечные присваивания, поскольку присваивания являются выражениями и имеют значения. В этом случае цепочечные присваивания можно реализовать, имея правоассоциативное присваивание , и присваивания происходят справа налево. Например, i = arr[i] = f()
эквивалентно arr[i] = f(); i = arr[i]
. В C++ они также доступны для значений типов классов путем объявления соответствующего возвращаемого типа для оператора присваивания.
В Python операторы присваивания не являются выражениями и, таким образом, не имеют значения. Вместо этого, цепочечные присваивания представляют собой ряд операторов с несколькими целями для одного выражения. Присваивания выполняются слева направо, так что i = arr[i] = f()
вычисляет выражение f()
, затем присваивает результат самой левой цели, i
, а затем присваивает тот же результат следующей цели, arr[i]
, используя новое значение i
. [9] Это по сути эквивалентно , tmp = f(); i = tmp; arr[i] = tmp
хотя для временного значения не создается фактическая переменная.
Некоторые языки программирования, такие как APL , Common Lisp , [10] Go , [11] JavaScript (начиная с версии 1.7), PHP , Maple , Lua , occam 2 , [12] Perl , [13] Python , [14] REBOL , Ruby , [15] и PowerShell, позволяют назначать несколько переменных параллельно, используя следующий синтаксис:
а, б := 0, 1
который одновременно присваивает 0 a
и 1 b
. Это чаще всего известно как параллельное присваивание ; оно было введено в CPL в 1963 году под названием одновременное присваивание , [16] и иногда называется множественным присваиванием , хотя это сбивает с толку при использовании с «одиночным присваиванием», поскольку они не являются противоположностями. Если правая часть присваивания представляет собой одну переменную (например, массив или структуру), эта функция называется распаковкой [17] или деструктурирующим присваиванием : [18]
список переменных := {0, 1}а, б := список
Список будет распакован так, что 0 будет присвоен a
, а 1 — b
. Кроме того,
а, б := б, а
меняет местами значения a
и b
. В языках без параллельного присваивания это должно быть написано так, чтобы использовать временную переменную
вар т := аа := бб := т
поскольку a := b; b := a
оставляет a
и b
с исходным значением b
.
Некоторые языки, такие как Go , F# и Python , объединяют параллельное присваивание, кортежи и автоматическую распаковку кортежей, чтобы разрешить возвращать несколько значений из одной функции, как в этом примере Python,
def f (): возврат 1 , 2 a , b = f ()
в то время как другие языки, такие как C# и Rust , показанные здесь, требуют явного построения и деконструкции кортежа с помощью скобок:
// Допустимый синтаксис C# или Rust ( a , b ) = ( b , a );
// C# кортеж return ( string , int ) f () => ( "foo" , 1 ); var ( a , b ) = f ();
// Возврат кортежа Rust let f = || ( "foo" , 1 ); let ( a , b ) = f ();
Это обеспечивает альтернативу использованию выходных параметров для возврата нескольких значений из функции. Это относится к CLU (1974), и CLU помог популяризировать параллельное присваивание в целом.
C# дополнительно допускает обобщенное деконструкционное присваивание с реализацией, определяемой выражением в правой части, поскольку компилятор ищет подходящий экземпляр или метод расширения Deconstruct
в выражении, который должен иметь выходные параметры для назначаемых переменных. [19] Например, один из таких методов, который даст классу, в котором он появляется, такое же поведение, как возвращаемое значение f()
выше, будет
void Deconstruct ( out string a , out int b ) { a = "foo" ; b = 1 ; }
В C и C++ оператор запятой похож на параллельное присваивание, позволяя выполнять несколько присваиваний в одном операторе, записывая a = 1, b = 2
вместо a, b = 1, 2
. Это в основном используется в циклах for и заменяется параллельным присваиванием в других языках, таких как Go. [20]
Однако приведенный выше код C++ не гарантирует идеальной одновременности, поскольку правая часть следующего кода a = b, b = a+1
вычисляется после левой части. В таких языках, как Python, a, b = b, a+1
будет присваивать две переменные одновременно, используя начальное значение a для вычисления нового b.
Использование знака равенства =
в качестве оператора присваивания часто подвергалось критике из-за конфликта с equals в качестве сравнения для равенства. Это приводит как к путанице у новичков при написании кода, так и к путанице даже у опытных программистов при чтении кода. Использование equals для присваивания восходит к языку Superplan Хайнца Рутисхаузера , разработанному с 1949 по 1951 год и особенно популяризированному Fortran:
Известным примером плохой идеи был выбор знака равенства для обозначения присваивания. Он появился в Fortran в 1957 году [a] и был слепо скопирован армиями разработчиков языка. Почему это плохая идея? Потому что она ниспровергает вековую традицию, согласно которой «=» обозначает сравнение на равенство, предикат, который может быть либо истинным, либо ложным. Но Fortran сделал его обозначающим присваивание, обеспечение равенства. В этом случае операнды находятся в неравных условиях: левый операнд (переменная) должен быть сделан равным правому операнду (выражению). x = y не означает то же самое, что y = x. [21]
— Никлаус Вирт , Хорошие идеи, Сквозь зеркало
Начинающие программисты иногда путают присваивание с реляционным оператором равенства, так как «=» означает равенство в математике и используется для присваивания во многих языках. Но присваивание изменяет значение переменной, в то время как проверка равенства проверяет, имеют ли два выражения одинаковое значение.
В некоторых языках, таких как BASIC , один знак равенства ( "="
) используется как для оператора присваивания, так и для оператора отношения равенства, с контекстом, определяющим, что имеется в виду. В других языках используются разные символы для двух операторов. [22] Например:
":="
), а оператор равенства — одиночное равенство ( "="
)."="
), а оператор равенства — пару знаков равенства ( "=="
).<-
, как в x <- value
, но в определенных контекстах может использоваться один знак равенства.Сходство двух символов может привести к ошибкам, если программист забудет, какая форма (" =
", " ==
", " :=
") является подходящей, или неправильно введет " ", когда предполагалось =
" ". Это распространенная проблема программирования с такими языками, как C (включая одну известную попытку бэкдора ядра Linux), [23] где оператор присваивания также возвращает назначенное значение (таким же образом, как функция возвращает значение) и может быть допустимо вложенным в выражения. Если , например, намерением было сравнить два значения в операторе, присваивание, скорее всего, вернет значение, интерпретируемое как логическое значение true, и в этом случае предложение будет выполнено, что приведет к неожиданному поведению программы. Некоторые языковые процессоры (например, gcc ) могут обнаруживать такие ситуации и предупреждать программиста о потенциальной ошибке. [24] [25]==
if
then
Два наиболее распространенных представления для копирования присваивания — это знак равенства ( =
) и двоеточие-равно ( :=
). Обе формы могут семантически обозначать либо оператор присваивания , либо оператор присваивания (который также имеет значение), в зависимости от языка и/или использования.
Другие возможности включают стрелку влево или ключевое слово, хотя есть и другие, более редкие варианты:
Математические псевдокодовые назначения обычно изображаются с помощью стрелки, направленной влево.
Некоторые платформы помещают выражение слева, а переменную справа:
Некоторые ориентированные на выражения языки, такие как Lisp [34] [35] и Tcl, единообразно используют префиксный (или постфиксный) синтаксис для всех операторов, включая присваивание.
=
предшествовало появлению Fortran, хотя и было популяризировано Fortran.