Отличия == и === в JavaScript
В JavaScript есть два похожих оператора: == и ===. Если не знать их отличия, это может обернуться кучей ошибок. Так что решил раскрыть эту тему. Чем именно отличаются == и ===, как они работают, почему так происходит, и как избежать ошибок.
Оператор == сравнивает на равенство, а вот === — на идентичность. Плюс оператора === состоит в том, что он не приводит два значения к одному типу. Именно из-за этого он обычно и используется.
abc == undefined; // true, если abc = undefined | null abc === undefined; // true - только если abc = undefined!
abc == false; // true, если abc = false | 0 | '' | [] abc === false; // true, только если abc = false!
Ведь путать false и 0 (или », или []) — вряд ли очень хорошо.
5 === 5; // true true === true; // true 'abc' === 'abc'; // true
А теперь интересный пример.
5 == 5; // true 5 === 5; // true new Number(5) == 5; // true new Number(5) === 5; // false!
Почему так происходит? Да, любое число — это объект класса Number. Но можно представить число как цифру — некоторой константой. Она единожды объявлена, и всегда идентична сама себе. Но в то же время объявляя новый объект класса Number — он равен ей по значению, но не идентичен (так как это два совершенно разных объекта класса Number).
Arrays / Objects
А вот для массивов и объектов оба оператора работают одинаково, сравнивая на идентичность:
var a = <>; a == <>; // false a === <>; // false a == a; // true a === a; // true
Для сравнения массивов и объектов можно написать специальную функцию:
function isEq(a, b) < if(a == b) return true; for(var i in a)< if(!isEq(a[i], b[i])) return false; >for(var i in b) < if(!isEq(a[i], b[i])) return false; >return true; >
Немножко неаккуратно, два цикла, да и про hasOwnProperty забыли; ну да сойдёт.
This
Есть ещё один подводный камень. Это передача в this.
(function()< this == 5; // true this === 5; // false >).call(5);
(function()< this == 5; // true this === 5; // false >).call(5);Вот такой вот момент. Стоит о нём не забывать.
Итого.
Ну а теперь представим, что мы пишем свой суперфреймворк, активно юзаем там оператор === вместо == просто потому что он красивее, и некто находит у нас несколько багов.
func(new Number(5)); (function()< func(this); >).call(5);
Кажется, что такие примеры нежизнеспособны? Пожалуйста!
$.each([1, 2, 3, 4, 5], function()< func(this); >);
Ну или захотелось расширить цифру.
var Five = new Number(5); Five.a = 2; // захотелось расширить, а просто 5 не расширяется // здесь как-то используем. func(Five);
На этом всё, надеюсь кому-то будет полезно. Спасибо за внимание.
UPD. Спасибо за ссылку vermilion1, JS Гарден.
- равенство
- идентичность
Операторы равенства — проверка того, равны ли два объекта или нет
Операторы == (равенство) и != (неравенство) проверяют равенство или неравенство своих операндов. Типы значений равны, если их содержимое равно. Ссылочные типы равны, если две переменные ссылаются на одно хранилище.
Оператор равенства ==
Оператор равенства == возвращает значение true , если его операнды равны. В противном случае возвращается значение false .
Равенство типов значений
Операнды встроенных типов значений равны, если равны их значения.
int a = 1 + 2 + 3; int b = 6; Console.WriteLine(a == b); // output: True char c1 = 'a'; char c2 = 'A'; Console.WriteLine(c1 == c2); // output: False Console.WriteLine(c1 == char.ToLower(c2)); // output: True
У операторов == , < , >, = , если какой-то из операндов не является числом (Double.NaN или Single.NaN), результатом операции является false . Это означает, что значение NaN не больше, не меньше и не равно любому другому значению double (или float ), включая NaN . Дополнительные сведения и примеры см. в справочных статьях по Double.NaN или Single.NaN.
Два операнда одного типа enum равны, если равны соответствующие значения базового целочисленного типа.
По умолчанию пользовательские типы struct не поддерживают оператор == . Чтобы поддерживать оператор == , пользовательская структура должна перегружать его.
Равенство ссылочных типов
По умолчанию два операнда ссылочного типа, отличные от записи, являются равными, если они ссылаются на один и тот же объект.
public class ReferenceTypesEquality < public class MyClass < private int id; public MyClass(int id) =>this.id = id; > public static void Main() < var a = new MyClass(1); var b = new MyClass(1); var c = a; Console.WriteLine(a == b); // output: False Console.WriteLine(a == c); // output: True >>
Как показано в примере, определяемые пользователем ссылочные типы поддерживают оператор == по умолчанию. Однако ссылочный тип может перегружать оператор == . Если ссылочный тип перегружает оператор == , воспользуйтесь методом Object.ReferenceEquals, чтобы проверить, что две ссылки этого типа указывают на один и тот же объект.
Равенство типов записей
Типы записей поддерживают == операторы != , которые по умолчанию предоставляют семантику равенства значений. То есть два операнда записи равны, когда оба они равны null или равны соответствующие значения всех полей и автоматически реализуемых свойств.
public class RecordTypesEquality < public record Point(int X, int Y, string Name); public record TaggedNumber(int Number, ListTags); public static void Main() < var p1 = new Point(2, 3, "A"); var p2 = new Point(1, 3, "B"); var p3 = new Point(2, 3, "A"); Console.WriteLine(p1 == p2); // output: False Console.WriteLine(p1 == p3); // output: True var n1 = new TaggedNumber(2, new List() < "A" >); var n2 = new TaggedNumber(2, new List() < "A" >); Console.WriteLine(n1 == n2); // output: False > >
Как показано в предыдущем примере, для элементов ссылочного типа, не относящихся к записи, сравниваются их ссылочные значения, а не для ссылочных экземпляров.
Равенство строк
Два операнда string равны, если они оба имеют значение null или оба экземпляра строки имеют одинаковую длину и идентичные символы в каждой позиции символа.
string s1 = "hello!"; string s2 = "HeLLo!"; Console.WriteLine(s1 == s2.ToLower()); // output: True string s3 = "Hello!"; Console.WriteLine(s1 == s3); // output: False
Сравнение равенства строк — это порядковые сравнения с учетом регистра. Дополнительные сведения о том, как сравнивать строки, см. в статье Сравнение строк в C#.
Равенство делегатов
Два операнда делегатов одного типа среды выполнения равны, если оба из них имеют значение null или их списки вызовов имеют одинаковую длину и содержат одинаковые записи в каждой позиции:
Action a = () => Console.WriteLine("a"); Action b = a + a; Action c = a + a; Console.WriteLine(object.ReferenceEquals(b, c)); // output: False Console.WriteLine(b == c); // output: True
Подробные сведения см. в разделе Delegate equality operators (Операторы равенства делегатов) в спецификации языка C#.
Делегаты, созданные из оценки семантических лямбда-выражений , не равны, как показано в следующем примере:
Action a = () => Console.WriteLine("a"); Action b = () => Console.WriteLine("a"); Console.WriteLine(a == b); // output: False Console.WriteLine(a + b == a + b); // output: True Console.WriteLine(b + a == a + b); // output: False
Оператор неравенства !=
Оператор неравенства != возвращается true , если его операнды не равны, false в противном случае. Для операндов встроенных типов выражение x != y дает тот же результат, что и выражение !(x == y) . Дополнительные сведения о равенстве типов см. в разделе Оператор равенства.
В следующем примере иллюстрируется использование оператора != .
int a = 1 + 1 + 2 + 3; int b = 6; Console.WriteLine(a != b); // output: True string s1 = "Hello"; string s2 = "Hello"; Console.WriteLine(s1 != s2); // output: False object o1 = 1; object o2 = 1; Console.WriteLine(o1 != o2); // output: True
Возможность перегрузки оператора
Определяемый пользователем тип может перегружать операторы == и != . Если тип перегружает один из двух операторов, он должен также перегружать и другой.
Тип записи не может явно перегружать == операторы и != операторы. Если необходимо изменить поведение операторов == и != для типа записи T , реализуйте метод IEquatable.Equals со следующей сигнатурой.
public virtual bool Equals(T? other);
Спецификация языка C#
Дополнительные сведения о равенстве типов записей см. в разделе Элементы равенствапредложения функции записей.
См. также
- справочник по C#
- Операторы и выражения C#
- System.IEquatable
- Object.Equals
- Object.ReferenceEquals
- Сравнения на равенство
- Операторы сравнения
Совместная работа с нами на GitHub
Источник этого содержимого можно найти на GitHub, где также можно создавать и просматривать проблемы и запросы на вытягивание. Дополнительные сведения см. в нашем руководстве для участников.
О сравнении объектов по значению — 2, или Особенности реализации метода Equals
В предыдущей публикации мы рассмотрели общие принципы реализации минимально необходимых доработок класса для возможности сравнения объектов класса по значению с помощью стандартной инфраструктуры платформы .NET.
Эти доработки включают перекрытие методов Object.Equals(Object) и Object.GetHashCode().
Остановимся подробнее на особенностях реализации метода Object.Equals(Object) для соответствия следующему требованию в документации:
x.Equals(y) returns the same value as y.Equals(x).
Класс Person, созданный в предыдущей публикации, содержит следующую реализацию метода Equals(Object):
Person.Equals(Object)
public override bool Equals(object obj)
После проверки ссылочного равенства текущего и входящего объекта, в случае отрицательного результата проверки, происходит приведение входящего объекта к типу Person для возможности сравнения объектов по значению.
В соответствии с примером, приведенным в документации, приведение производится с помощью оператора as. Проверим, дает ли это корректный результат.
Реализуем класс PersonEx, унаследовав класс Person, добавив в персональные данные свойство Middle Name, и перекрыв соответствующим образом методы Person.Equals(Object) и Person.GetHashCode().
class PersonEx
using System; namespace HelloEquatable < public class PersonEx : Person < public string MiddleName < get; >public PersonEx( string firstName, string middleName, string lastName, DateTime? birthDate ) : base(firstName, lastName, birthDate) < this.MiddleName = NormalizeName(middleName); >public override int GetHashCode() => base.GetHashCode() ^ this.MiddleName.GetHashCode(); protected static bool EqualsHelper(PersonEx first, PersonEx second) => EqualsHelper((Person)first, (Person)second) && first.MiddleName == second.MiddleName; public override bool Equals(object obj) < if ((object)this == obj) return true; var other = obj as PersonEx; if ((object)other == null) return false; return EqualsHelper(this, other); >> >
Легко заметить, что если у объекта класса Person вызвать метод Equals(Object) и передать в него объект класса PersonEx, то, если у этих объектов (персон) совпадают имя, фамилия и дата рождения, метод Equals возвратит true, в противном случае метод возвратит false.
(При выполнении метода Equals, входящий объект, имеющий во время выполнения (runtime) тип PersonEx, будет успешно приведен к типу Person с помощью оператора as, и далее будет произведено сравнение объектов по значениям полей, имеющихся только в классе Person, и будет возвращен соответствующий результат.)
Очевидно, что с предметной точки зрения это неверное поведение:
Совпадение имени, фамилии и даты рождения не означает, что это одна и та же персона, т.к. у одной персоны отсутствует атрибут middle name (речь не о неопределенном значении атрибута, а об отсутствии самого атрибута), а у другой имеется атрибут middle name.
(Это разные типы сущностей.)
Если же, напротив, у объекта класса PersonEx вызвать метод Equals(Object) и передать в него объект класса Person, то метод Equals в любом случае возвратит false, независимо от значений свойств объектов.
(При выполнении метода Equals, входящий объект, имеющий во время выполнения (runtime) тип Person, не будет успешно приведен к типу PersonEx с помощью оператора as — результатом приведения будет null, и метод возвратит false.)
Здесь мы наблюдаем верное с предметной точки зрения поведение, в отличие от предыдущего случая.
Эти виды поведения можно легко проверить, выполнив следующий код:
var person = new Person("John", "Smith", new DateTime(1990, 1, 1)); var personEx = new PersonEx("John", "Teddy", "Smith", new DateTime(1990, 1, 1)); bool isSamePerson = person.Equals(personEx); bool isSamePerson2 = personEx.Equals(person);
Однако, в разрезе данной публикации нас в большей степени интересует соответствие реализованного поведения Equals(Object) требованиям в документации, нежели корректность логики с предметной точки зрения.
А именно соответствие требованию:
x.Equals(y) returns the same value as y.Equals(x).
Это требование не выполняется.
(А с точки зрения здравого смысла, какие могут быть проблемы при текущей реализации Equals(Object)?
У разработчика типа данных нет информации, каким именно способом будут сравниваться объекты — x.Equals(y) или y.Equals(x) — как в клиентском коде (при явном вызове Equals), так и при помещении объектов в хеш-наборы (хеш-карты) и словари (внутри самих наборов/словарей).
В этом случае поведение программы будет недетерминировано, и зависеть от деталей реализации.)
Рассмотрим, каким именно образом можно реализовать метод Equals(Object), обеспечив ожидаемое поведение.
На текущий момент представляется корректным способ, предложенный Джеффри Рихтером (Jeffrey Richter) в книге CLR via C# (Part II «Designing Types», Chapter 5 «Primitive, Reference, and Value Types», Subchapter «Object Equality and Identity»), когда перед сравнением объектов непосредственно по значению, типы объектов во время выполнения (runtime), полученные с помощью метода Object.GetType() проверяются на равенство (вместо односторонних проверки на совместимость и приведения типов объектов с помощью оператора as):
if (this.GetType() != obj.GetType()) return false;
Следует отметить, что использование данного способа не является однозначным, т.к. существует три различных способа проверки на равенство экземпляров класса Type, с теоретически различными результатами для одних и тех же операндов:
For two objects x and y that have identical runtime types, Object.ReferenceEquals(x.GetType(),y.GetType()) returns true.
Таким образом, объекты класса Type можно проверить на равенство с помощью сравнения по ссылке:
bool isSameType = (object)obj1.GetType() == (object)obj2.GetType();
bool isSameType = Object.ReferenceEquals(obj1.GetType(), obj2.GetType());
2. Класс Type имеет методы Equals(Object) и Equals(Type), поведение которых определено следующим образом:
Determines if the underlying system type of the current Type object is the same as the underlying system type of the specified Object.
Return Value
Type: System.Boolean
true if the underlying system type of o is the same as the underlying system type of the current Type; otherwise, false. This method also returns false if:
o is null.
o cannot be cast or converted to a Type object.
Remarks
This method overrides Object.Equals. It casts o to an object of type Type and calls the Type.Equals(Type) method.
Determines if the underlying system type of the current Type is the same as the underlying system type of the specified Type.
Return Value
Type: System.Boolean
true if the underlying system type of o is the same as the underlying system type of the current Type; otherwise, false.
Внутри эти методы реализованы следующим образом:
public override bool Equals(Object o)
public virtual bool Equals(Type o)
Как видим, результат выполнения обоих методов Equals для объектов класса Type в общем случае может отличаться от сравнения объектов по ссылке, т.к. в случае использования методов Equals, сравниваются по ссылке не сами объекты класса Type, а их свойства UnderlyingSystemType, относящиеся к тому же классу.
Однако, из описания методов Equals класса Type.Equals(Object) представляется, что они не предназначены для сравнения непосредственно объектов класса Type.
Примечание:
Для метода Type.Equals(Object) проблема несоответствия требованию (как следствие использования оператора as)
x.Equals(y) returns the same value as y.Equals(x).
не возникнет, если только в потомках класса Type метод не будет перекрыт некорректным образом.
Для предотвращения этой потенциальной проблемы, возможно, разработчиками было бы целесообразно объявить метод как sealed.
3. Класс Type, начиная с .NET Framework 4.0, имеет перегруженные операторы == или !=, поведение которых описывается простым образом, без описания деталей реализации:
Indicates whether two Type objects are equal.
Return Value
Type: System.Boolean
true if left is equal to right; otherwise, false.
Indicates whether two Type objects are not equal.
Return Value
Type: System.Boolean
true if left is not equal to right; otherwise, false.
Изучение исходных кодов тоже не дает информации по деталям реализации, для выяснения внутренней логики операторов:
public static extern bool operator ==(Type left, Type right);
public static extern bool operator !=(Type left, Type right);
Реализуем классы Person и PersonEx соответствующим образом:
class Person (with new Equals method)
using System; namespace HelloEquatable < public class Person < protected static string NormalizeName(string name) =>name?.Trim() ?? string.Empty; protected static DateTime? NormalizeDate(DateTime? date) => date?.Date; public string FirstName < get; >public string LastName < get; >public DateTime? BirthDate < get; >public Person(string firstName, string lastName, DateTime? birthDate) < this.FirstName = NormalizeName(firstName); this.LastName = NormalizeName(lastName); this.BirthDate = NormalizeDate(birthDate); >public override int GetHashCode() => this.FirstName.GetHashCode() ^ this.LastName.GetHashCode() ^ this.BirthDate.GetHashCode(); protected static bool EqualsHelper(Person first, Person second) => first.BirthDate == second.BirthDate && first.FirstName == second.FirstName && first.LastName == second.LastName; public override bool Equals(object obj) < if ((object)this == obj) return true; if (obj == null) return false; if (this.GetType() != obj.GetType()) return false; return EqualsHelper(this, (Person)obj); >> >
class PersonEx (with new Equals method)
using System; namespace HelloEquatable < public class PersonEx : Person < public string MiddleName < get; >public PersonEx( string firstName, string middleName, string lastName, DateTime? birthDate ) : base(firstName, lastName, birthDate) < this.MiddleName = NormalizeName(middleName); >public override int GetHashCode() => base.GetHashCode() ^ this.MiddleName.GetHashCode(); protected static bool EqualsHelper(PersonEx first, PersonEx second) => EqualsHelper((Person)first, (Person)second) && first.MiddleName == second.MiddleName; public override bool Equals(object obj) < if ((object)this == obj) return true; if (obj == null) return false; if (this.GetType() != obj.GetType()) return false; return EqualsHelper(this, (PersonEx)obj); >> >
Теперь следующее требование к реализации метода Equals(Object) будет соблюдаться:
x.Equals(y) returns the same value as y.Equals(x).
что легко проверяется выполнением кода:
var person = new Person("John", "Smith", new DateTime(1990, 1, 1)); var personEx = new PersonEx("John", "Teddy", "Smith", new DateTime(1990, 1, 1)); bool isSamePerson = person.Equals(personEx); bool isSamePerson2 = personEx.Equals(person);
Примечания к реализации метода Equals(Object):
- вначале проверяются на равенство ссылки, указывающие на текущий и входящий объекты, и, в случае совпадения ссылок, возвращается true;
- затем проверяется на null ссылка на входящий объект, и, в случае положительного результата проверки, возвращается false;
- затем проверяется идентичность типов текущего и входящего объекта, и, в случае отрицательного результата проверки, возвращается false;
- на последнем этапе производятся приведение входящего объекта к типу данного класса и непосредственно сравнение объектов по значению.
Таким образом, мы нашли оптимальный способ реализации ожидаемого поведения метода Equals(Object).
На десерт проверим корректность реализации Equals(Object) в стандартной библиотеке.
Compares two Uri instances for equality.
Syntax
public override bool Equals(object comparand)
Parameters
comparand
Type: System.Object
The Uri instance or a URI identifier to compare with the current instance.
Return Value
Type: System.Boolean
A Boolean value that is true if the two instances represent the same URI; otherwise, false.
Uri.Equals(Object)
public override bool Equals(object comparand) < if ((object)comparand == null) < return false; >if ((object)this == (object)comparand) < return true; >Uri obj = comparand as Uri; // // we allow comparisons of Uri and String objects only. If a string // is passed, convert to Uri. This is inefficient, but allows us to // canonicalize the comparand, making comparison possible // if ((object)obj == null) < string s = comparand as string; if ((object)s == null) return false; if (!TryCreate(s, UriKind.RelativeOrAbsolute, out obj)) return false; >// method code . >
Логично предположить, что следующее требование к реализации метода Equals(Object) не выполняется:
x.Equals(y) returns the same value as y.Equals(x).
Т.к. класс String и метод String.Equals(Object), в свою очередь, не «знают» о существовании класса Uri.
Это легко проверить на практике, выполнив код:
const string uriString = "https://www.habrahabr.ru"; Uri uri = new Uri(uriString); bool isSameUri = uri.Equals(uriString); bool isSameUri2 = uriString.Equals(uri);
Сравнение чисел с плавающей точкой
По причинам, связанным со способом внутреннего представления чисел с плавающей точкой ( float ), не нужно проверять два числа с плавающей точкой ( float ) на равенство.
Подробнее об этом можно узнать в документации по типу float .
Замечание: Когда пишут код, помнят, что жонглирование типами в PHP не всегда даёт предсказуемый результат при сравнении значений разных типов, особенно при сравнении целых чисел ( int ) с логическими значениями ( bool ) или целых чисел ( int ) со строками ( string ). Поэтому лучше пользоваться операторами === и !== , а не == и != .
Несравнимые значение
Хотя тождественные сравнения ( === и !== ) можно применять к произвольным значениям, другие операторы сравнения лучше применять только к сравнимым значениям. Результат сравнения несравнимых значений не определён и на него не нужно полагаться.
Смотрите также
Тернарный оператор
Ещё один условный оператор — тернарный оператор «?:».
Пример #3 Присваивание значения по умолчанию
// Пример выражения с тернарным оператором
$action = (empty( $_POST [ ‘action’ ])) ? ‘default’ : $_POST [ ‘action’ ];
// Код выше аналогичен блоку с конструкциями if/else
if (empty( $_POST [ ‘action’ ])) $action = ‘default’ ;
> else $action = $_POST [ ‘action’ ];
>
Выражение (expr1) ? (expr2) : (expr3) интерпретируется как expr2 , если expr1 равно true , или как expr3 , если expr1 равно false .
Можно не писать среднюю часть тернарного оператора. Выражение expr1 ?: expr3 оценивается как результат выражения expr1 , если оно оценивается как true , иначе как результат выражения expr3 . Выражение expr1 оценивается только один раз.
Замечание: Обратите внимание, что тернарный оператор — это выражение, и он оценивается не как переменная, а как результат выражения. Это важно, если нужно вернуть переменную по ссылке. Выражение return $var == 42 ? $a : $b; не будет работать в функции, возвращающей значение по ссылке, а в более поздних версиях PHP также будет выдано предупреждение.
Замечание:
Рекомендовано избегать «нагромождения» тернарных выражений. Поведение PHP при указании более чем одного тернарного оператора без скобок в одном выражении неочевидно в сравнении с другими языками. Впрямь, до PHP 8.0.0 троичные выражения оценивались как левоассоциативные, а не правоассоциативные, как в большей части других языков программирования. Опора на левую ассоциативность устарела начиная с PHP 7.4.0. Начиная с PHP 8.0.0 тернарный оператор неассоциативен.
Пример #4 Неочевидное поведение тернарного оператора
// кажется, что следующий код выведет «true»
echo ( true ? ‘true’ : false ? ‘t’ : ‘f’ );
// однако он выводит «t» до PHP 8.0.0
// это потому, что тернарные выражения левоассоциативны
// следующая запись — более очевидная версия того же кода, который показан выше
echo (( true ? ‘true’ : false ) ? ‘t’ : ‘f’ );
// здесь видно, что первое выражение оценивается как строковое «true», которое
// оценивается как логическое (bool) true, поэтому возвращает истинную ветвь
// второго тернарного выражения.
Замечание:
Цепочка коротких тернарных операторов ( ?: ), однако, стабильна и ведёт себя обоснованно. Она будет оценивать первый аргумент, который оценивается как не ложное значение. Обратите внимание, что неопределённые значения все равно вызовут предупреждение.
Пример #5 Цепочка коротких тернарных операторов
echo 0 ?: 1 ?: 2 ?: 3 , PHP_EOL ; // 1
echo 0 ?: 0 ?: 2 ?: 3 , PHP_EOL ; // 2
echo 0 ?: 0 ?: 0 ?: 3 , PHP_EOL ; // 3
?>?php
Оператор объединения с null
Другой полезный сокращённый оператор — это оператор объединения с NULL — «??» (null coalescing).
Пример #6 Присваивание значения по умолчанию
// Пример работы с оператором нулевого слияния
$action = $_POST [ ‘action’ ] ?? ‘default’ ;
// Пример выше аналогичен этому выражению с if/else
if (isset( $_POST [ ‘action’ ])) $action = $_POST [ ‘action’ ];
> else $action = ‘default’ ;
>
Выражение (expr1) ?? (expr2) вычисляется так: expr2 , если expr1 равно null , иначе expr1 .
Этот оператор не вызывает предупреждения или ошибки, если левый операнд не существует, точно как языковая конструкция isset() . Это очень полезно для ключей массива.
Замечание: Обратите внимание, оператор объединения с NULL — это выражение, и он оценивается не как переменная, а как результат вычисления выражения. Это важно, если нужно вернуть значение по ссылке. Выражение return $foo ?? $bar; в функции, возвращающей ссылку, будет не работать, а выводить предупреждение.
Замечание:
У оператора объединения с NULL низкий приоритет. То есть при смешивании его с другими операторами (например, с операторами конкатенации строк или арифметическими операторами), скорее всего, потребуются круглые скобки.
// Вызывает предупреждение о том, что $name не определено.
print ‘Mr. ‘ . $name ?? ‘Anonymous’ ;
// Выведет «Mr. Anonymous»
print ‘Mr. ‘ . ( $name ?? ‘Anonymous’ );
Замечание:
Обратите внимание, оператор объединения с NULL разрешает простую вложенность:
Пример #7 Вложенный оператор null coalescing
$foo = null ;
$bar = null ;
$baz = 1 ;
$qux = 2 ;
echo $foo ?? $bar ?? $baz ?? $qux ; // выведет 1