тег документации
Тег позволяет описать методы доступа к свойству и свойствам. При добавлении свойства с помощью мастера кода в интегрированной среде разработки Visual Studio он добавит тег для нового свойства. Необходимо вручную добавить тег, чтобы описать значение, которое представляет свойство.
Синтаксис
/// property-description
Параметры
property-description
Описание свойства.
Замечания
Скомпилируйте их для /doc обработки примечаний документации к файлу.
Пример
// xml_value_tag.cpp // compile with: /LD /clr /doc // post-build command: xdcmake xml_value_tag.dll using namespace System; /// Text for class Employee. public ref class Employee < private: String ^ name; /// Name accesses the value of the name data member public: property String ^ Name < String ^ get() < return name; >void set(String ^ i) < name = i; >> >;
См. также
Обратная связь
Были ли сведения на этой странице полезными?
value (Справочник по C#)
Контекстное ключевое слово value используется в методе доступа set в объявлениях свойства и индексатора. Оно аналогично входному параметру метода. Ключевое слово value ссылается на значение, которое клиентский код пытается присвоить свойству или индексатору. В приведенном ниже примере класс MyDerivedClass имеет свойство с именем Name , в котором используется параметр value для присвоения новой строки резервному полю name . С точки зрения клиентского кода эта операция выглядит как простое присвоение.
class MyBaseClass < // virtual auto-implemented property. Overrides can only // provide specialized behavior if they implement get and set accessors. public virtual string Name < get; set; >// ordinary virtual property with backing field private int _num; public virtual int Number < get < return _num; >set < _num = value; >> > class MyDerivedClass : MyBaseClass < private string _name; // Override auto-implemented property with ordinary property // to provide specialized accessor behavior. public override string Name < get < return _name; >set < if (!string.IsNullOrEmpty(value)) < _name = value; >else < _name = "Unknown"; >> > >
Дополнительные сведения см. в статьях Свойства и Индексаторы.
Спецификация языка C#
Дополнительные сведения см. в спецификации языка C#. Спецификация языка является предписывающим источником информации о синтаксисе и использовании языка C#.
См. также
- Справочник по C#
- Руководство по программированию на C#
- Ключевые слова в C#
Совместная работа с нами на GitHub
Источник этого содержимого можно найти на GitHub, где также можно создавать и просматривать проблемы и запросы на вытягивание. Дополнительные сведения см. в нашем руководстве для участников.
Что такое value в c
Кроме обычных методов в языке C# предусмотрены специальные методы доступа, которые называют свойства . Они обеспечивают простой доступ к полям классов и структур, узнать их значение или выполнить их установку.
Определение свойств
Стандартное описание свойства имеет следующий синтаксис:
[модификаторы] тип_свойства название_свойства < get < действия, выполняемые при получении значения свойства>set < действия, выполняемые при установке значения свойства>>
Вначале определения свойства могут идти различные модификаторы, в частности, модификаторы доступа. Затем указывается тип свойства, после которого идет название свойства. Полное определение свойства содержит два блока: get и set .
В блоке get выполняются действия по получению значения свойства. В этом блоке с помощью оператора return возвращаем некоторое значение.
В блоке set устанавливается значение свойства. В этом блоке с помощью параметра value мы можем получить значение, которое передано свойству.
Блоки get и set еще называются акссесорами или методами доступа (к значению свойства), а также геттером и сеттером.
Person person = new Person(); // Устанавливаем свойство - срабатывает блок Set // значение "Tom" и есть передаваемое в свойство value person.Name = "Tom"; // Получаем значение свойства и присваиваем его переменной - срабатывает блок Get string personName = person.Name; Console.WriteLine(personName); // Tom class Person < private string name = "Undefined"; public string Name < get < return name; // возвращаем значение свойства >set < name = value; // устанавливаем новое значение свойства >> >
Здесь в классе Person определено приватное поле name , которая хранит имя пользователя, и есть общедоступное свойство Name . Хотя они имеют практически одинаковое название за исключением регистра, но это не более чем стиль, названия у них могут быть произвольные и не обязательно должны совпадать.
Через это свойство мы можем управлять доступом к переменной name . В свойстве в блоке get возвращаем значение поля:
А в блоке set устанавливаем значение переменной name. Параметр value представляет передаваемое значение, которое передается переменной name.
В программе мы можем обращаться к этому свойству, как к обычному полю. Если мы ему присваиваем какое-нибудь значение, то срабатывает блок set , а передаваемое значение передается в параметр value :
person.Name = "Tom";
Если мы получаем значение свойства, то срабатывает блок get , который по сути возвращает значение переменной name:
string personName = person.Name;
То есть по сути свойство Name ничего не хранит, оно выступает в роли посредника между внешним кодом и переменной name.
Возможно, может возникнуть вопрос, зачем нужны свойства, если мы можем в данной ситуации обходиться обычными полями класса? Но свойства позволяют вложить дополнительную логику, которая может быть необходима при установке или получении значения. Например, нам надо установить проверку по возрасту:
Person person = new Person(); Console.WriteLine(person.Age); // 1 // изменяем значение свойства person.Age = 37; Console.WriteLine(person.Age); // 37 // пробуем передать недопустимое значение person.Age = -23; // Возраст должен быть в диапазоне от 1 до 120 Console.WriteLine(person.Age); // 37 - возраст не изменился class Person < int age = 1; public int Age < set < if (value < 1 || value >120) Console.WriteLine("Возраст должен быть в диапазоне от 1 до 120"); else age = value; > get < return age; >> >
В данном случае переменная age хранит возраст пользователя. Напрямую мы не можем обратиться к этой переменной — только через свойство Age. Причем в блоке set мы устанавливаем значение, если оно соответствует некоторому разумному диапазону. Поэтому при передаче свойству Age значения, которое не входит в этот диапазон, значение переменной не будет изменяться:
person.Age = -23;Консольный вывод программы:
1 37 Возраст должен быть в диапазоне от 1 до 120 37
Таким образом, свойство позволяет опосредовать и контролировать доступ к данным объекта.
Свойства только для чтения и записи
Блоки set и get не обязательно одновременно должны присутствовать в свойстве. Если свойство определяет только блок get , то такое свойство доступно только для чтения — мы можем получить его значение, но не установить.
И, наоборот, если свойство имеет только блок set , тогда это свойство доступно только для записи — можно только установить значение, но нельзя получить:
Person person = new Person(); // свойство для чтения - можно получить значение Console.WriteLine(person.Name); // Tom // но нельзя установить // person.Name = "Bob"; // ! Ошибка // свойство для записи - можно устновить значение person.Age = 37; // но нелзя получить // Console.WriteLine(person.Age); // ! Ошибка person.Print(); class Person < string name = "Tom"; int age = 1; // свойство только для записи public int Age < set < age = value; >> // свойство только для чтения public string Name < get < return name; >> public void Print()=> Console.WriteLine($"Name: Age: "); >
Здесь свойство Name доступно только для чтения, поскольку оно имеет только блок get :
public string Name < get < return name; >>
Мы можем получить его значение, но НЕ можем установить:
Console.WriteLine(person.Name); // получить можно person.Name = "Bob"; // ! Ошибка - установить нельзя
А свойство Age, наоборот, доступно только для записи, поскольку оно имеет только блок set :
public int Age < set < age = value; >>
Можно установить его значение, но нельзя получить:
person.Age = 37; // установить можно Console.WriteLine(person.Age); // ! Ошибка - получить значение нельзя
Вычисляемые свойства
Свойства необязательно связаны с определенной переменной. Они могут вычисляться на основе различных выражений
Person tom = new("Tom", "Smith"); Console.WriteLine(tom.Name); // Tom Smith class Person < string firstName; string lastName; public string Name < get < return $""; > > public Person(string firstName, string lastName) < this.firstName = firstName; this.lastName = lastName; >>
В данном случае класс Person имеет свойство Name, которое доступно только для чтения и которое возвращает общее значение на основе значений переменных firstName и lastName.
Модификаторы доступа
Мы можем применять модификаторы доступа не только ко всему свойству, но и к отдельным блокам get и set:
Person tom = new("Tom"); // Ошибка - set объявлен с модификатором private //tom.Name = "Bob"; Console.WriteLine(tom.Name); // Tom class Person < string name = ""; public string Name < get < return name; >private set < name = value; >> public Person(string name) => Name = name; >
Теперь закрытый блок set мы сможем использовать только в данном классе — в его методах, свойствах, конструкторе, но никак не в другом классе:
При использовании модификаторов в свойствах следует учитывать ряд ограничений:
- Модификатор для блока set или get можно установить, если свойство имеет оба блока (и set, и get)
- Только один блок set или get может иметь модификатор доступа, но не оба сразу
- Модификатор доступа блока set или get должен быть более ограничивающим, чем модификатор доступа свойства. Например, если свойство имеет модификатор public, то блок set/get может иметь только модификаторы protected internal, internal, protected, private protected и private
Автоматические свойства
Свойства управляют доступом к полям класса. Однако что, если у нас с десяток и более полей, то определять каждое поле и писать для него однотипное свойство было бы утомительно. Поэтому в .NET были добавлены автоматические свойства. Они имеют сокращенное объявление:
class Person < public string Name < get; set; >public int Age < get; set; >public Person(string name, int age) < Name = name; Age = age; >>
На самом деле тут также создаются поля для свойств, только их создает не программист в коде, а компилятор автоматически генерирует при компиляции.
В чем преимущество автосвойств, если по сути они просто обращаются к автоматически создаваемой переменной, почему бы напрямую не обратиться к переменной без автосвойств? Дело в том, что в любой момент времени при необходимости мы можем развернуть автосвойство в обычное свойство, добавить в него какую-то определенную логику.
Стоит учитывать, что нельзя создать автоматическое свойство только для записи, как в случае со стандартными свойствами.
Автосвойствам можно присвоить значения по умолчанию (инициализация автосвойств):
Person tom = new(); Console.WriteLine(tom.Name); // Tom Console.WriteLine(tom.Age); // 37 class Person < public string Name < get; set; >= "Tom"; public int Age < get; set; >= 37; >
И если мы не укажем для объекта Person значения свойств Name и Age, то будут действовать значения по умолчанию.
Автосвойства также могут иметь модификаторы доступа:
class Person < public string Name < private set; get;>public Person(string name) => Name = name; >
Мы можем убрать блок set и сделать автосвойство доступным только для чтения. В этом случае для хранения значения этого свойства для него неявно будет создаваться поле с модификатором readonly, поэтому следует учитывать, что подобные get-свойства можно установить либо из конструктора класса, как в примере выше, либо при инициализации свойства:
class Person < // через инициализацию свойства public string Name < get; >= "Tom"; // через конструктор public Person(string name) => Name = name; >
Блок init
Начиная с версии C# 9.0 сеттеры в свойствах могут определяться с помощью оператора init (от слова «инициализация» — это есть блок init призван инициализировать свойство). Для установки значений свойств с init можно использовать только инициализатор, либо конструктор, либо при объявлении указать для него значение. После инициализации значений подобных свойств их значения изменить нельзя — они доступны только для чтения. В этом плане init-свойства сближаются со свойствами для чтения. Разница состоит в том, что init-свойства мы также можем установить в инициализаторе (свойства для чтения установить в инициализаторе нельзя). Например:
Person person = new(); //person.Name = "Bob"; //! Ошибка - после инициализации изменить значение нельзя Console.WriteLine(person.Name); // Undefined public class Person < public string Name < get; init; >= "Undefined"; >
В данном случае класс Person для свойства Name вместо сеттера использует оператор init . В итоге на строке
Person person = new();
предполагается создание объекта с инициализацией всех его свойств. В данном случае свойство Name получит в качестве значения строку «Undefined». Однако поскольку инициализация свойства уже произошла, то на строке
person.Name = "Bob"; // Ошибка
мы получим ошибку.
Как можно установить подобное свойство? Выше продемонстрирован один из способов — установка значения при определении свойства. Второй способ — через конструктор:
Person person = new("Tom"); Console.WriteLine(person.Name); // Tom public class Person < public Person(string name) =>Name = name; public string Name < get; init; >>
Третий способ — через инициализатор:
Person person = new() < Name = "Bob">; Console.WriteLine(person.Name); // Bob public class Person < public string Name < get; init; >= ""; >
В принцпе есть еще четвертый способ — установка через другое свойство с модификатором init :
var person = new Person() < Name = "Sam" >; Console.WriteLine(person.Name); // Sam Console.WriteLine(person.Email); // Sam@gmail.com public class Person < string name = ""; public string Name < get < return name; >init < name = value; Email = $"@gmail.com"; > > public string Email < get; init; >= ""; >
В данном случае свойство Name управляет полем для чтения name . Благодаря этому перед установкой значения свойства мы можем произвести некоторую предобработку. Кроме того, в выражении init устанавливается другое init-свойство — Email, которое для установки значения использует значение свойства Name — из имени получаем значение для электронного адреса.
Причем если при объявлении свойства указано значение, то в конструкторе мы можем его изменить. Значение, установленное в конструкторе, можно изменить в инициализаторе. Однако дальше процесс инициализации заканчивается. И значение не может быть изменено.
Сокращенная запись свойств
Как и методы, мы можем сокращать определения свойств. Поскольку блоки get и set представляют специальные методы, то как и обычные методы, если они содержат одну инструкцию, то мы их можем сократить с помощью оператора => :
class Person < string name; public string Name < get =>name; set => name = value; > >
Также можно сокращать все свойство в целом:
class Person < string name; // эквивалентно public string Name < get < return name; >> public string Name => name; >
модификатор required
Модификатор required (добавлен в C# 11) указывает, что поле или свойства с этим модификатором обязательно должны быть инициализированы. Например, в следующем примере мы получим ошибку:
Person tom = new Person(); // ошибка - свойства Name и Age не инициализированы public class Person < public required string Name < get; set; >public required int Age < get; set; >>
Здесь свойства Name и Age отмечены как обязательные для инициализации с помощью модификатора required , поэтому необходимо использовать инициализатор для их инициализации:
Person tom = new Person < Name = "Tom", Age = 38 >; // ошибки нет
Причем не важно, устанавливаем эти свойства в конструкторе или инициализируем при определении, все равно надо использовать инициализатор для установки их значений. Например, в следующем примере мы получим ошибку:
Person bob = new Person("Bob"); // ошибка - свойства Name и Age все равно надо установить в инициализаторе public class Person < public Person(string name) < Name = name; >public required string Name < get; set; >public required int Age < get; set; >= 22; >
Категории выражений в C++
Категории выражений, такие как lvalue и rvalue, относятся, скорее, к фундаментальным теоретическим понятиям языка C++, чем к практическим аспектам его использования. По этой причине многие даже опытные программисты достаточно смутно представляют себе, что они означают. В этой статье я постараюсь максимально просто объяснить значение этих терминов, разбавляя теорию практическими примерами. Сразу оговорюсь: статья не претендует на максимально полное и строгое описание категорий выражений, за подробностями я рекомендую обращаться непосредственно в первоисточник: Стандарт языка C++.
В статье будет довольно много англоязычных терминов, связано это с тем, что некоторые из них сложно перевести на русский, а другие переводятся в разных источниках по-разному. Поэтому я буду часто указывать англоязычные термины, выделяя их курсивом.
Немного истории
Термины lvalue и rvalue появились ещё в языке C. Стоит отметить, что путаница была заложена в терминологию изначально, потому как относятся они к выражениям (expressions), а не к значениям (values). Исторически lvalue – это то, что может быть слева (left) от оператора присваивания, а rvalue – то, что может быть только справа (right).
lvalue = rvalue;
Однако, такое определение несколько упрощает и искажает суть. Стандарт C89 определял lvalue как object locator, т.е. объект с идентифицируемым местом в памяти. Соответственно, всё, что не подходило под это определение, входило в категорию rvalue.
Бьярн спешит на помощь
В языке C++ терминология категорий выражений достаточно сильно эволюционировала, в особенности после принятия Стандарта C++11, где вводились понятия rvalue-ссылок и семантики перемещения (move semantics). История появления новой терминологии интересно описана в статье Страуструпа “New” Value Terminology.
В основу новой более строгой терминологии легли 2 свойства:
- наличие идентичности (identity) – т. е. какого-то параметра, по которому можно понять, ссылаются ли два выражения на одну и ту же сущность или нет (например, адрес в памяти);
- возможность перемещения (can be moved from) – поддерживает семантику перемещения.
Обладающие идентичностью выражения обобщены под термином glvalue (generalized values), перемещаемые выражения называются rvalue. Комбинации двух этих свойств определили 3 основные категории выражений:
Обладают идентичностью | Лишены идентичности | |
---|---|---|
Не могут быть перемещены | lvalue | – |
Могут быть перемещены | xvalue | prvalue |
На самом деле, в Стандарте C++17 появилось понятие избегание копирования (copy elision) – формализация ситуаций, когда компилятор может и должен избегать копирования и перемещения объектов. В связи с этим, prvalue не обязательно могут быть перемещены. Подробно и с примерами об этом можно почитать вот тут. Впрочем, это не влияет на понимание общей схемы категорий выражений.
В современном Стандарте C++ структура категорий приводится в виде вот такой схемы:
Разберём в общих чертах свойства категорий, а также выражения языка, которые входят в каждую из категорий. Сразу отмечу, что приведённые ниже списки выражений для каждой категории не могут считаться полными, для более точной и подробной информации следует обратиться напрямую к Стандарту C++.
glvalue
Выражения категории glvalue обладают следующими свойствами:
- могут быть неявно преобразованы в prvalue;
- могут быть полиморфными, т. е. для них имеют смысл понятия статического и динамического типа;
- не могут иметь тип void – это напрямую следует из свойства наличия идентичности, ведь для выражений типа void нет такого параметра, который позволил бы отличать их одно от другого;
- могут иметь неполный тип (incomplete type), например, в виде forward declaration (если это разрешено для конкретного выражения).
rvalue
Выражения категории rvalue обладают следующими свойствами:
- нельзя получить адрес rvalue в памяти – это напрямую следует из свойства отсутствия идентичности;
- не могут находиться в левой части оператора присваивания или составного присваивания;
- могут использоваться для инициализации константной lvalue-ссылки или rvalue-ссылки, при этом время жизни объекта расширяется до времени жизни ссылки;
- если используются как аргумент при вызове функции, у которой есть 2 перегруженные версии: одна принимает константную lvalue-ссылку, а другая – rvalue-ссылку, то выбирается версия, принимающая rvalue-ссылку. Именно это свойство используется при реализации семантики перемещения (move semantics):
class A < public: A() = default; A(const A&) < std::cout A(A&&) < std::cout >; . A a; A b(a); // Вызывается A(const A&) A c(std::move(a)); // Вызывается A(A&&)
Технически, A&& является rvalue и может использоваться для инициализации как константной lvalue-ссылки, так и rvalue-ссылки. Но благодаря этому свойству никакой неоднозначности нет, выбирается вариант конструктора, принимающий rvalue-ссылку.
lvalue
- все свойства glvalue (см. выше);
- можно взять адрес (используя встроенный унарный оператор & );
- модифицируемые lvalue могут находиться в левой части оператора присваивания или составных операторов присваивания;
- могут использоваться для инициализации ссылки на lvalue (как константной, так и неконстантной).
К категории lvalue относятся следующие выражения:
- имя переменной, функции или поле класса любого типа. Даже если переменная является rvalue-ссылкой, имя этой переменной в выражении является lvalue;
void func() <> . auto* func_ptr = &func; // порядок: получаем указатель на функцию auto& func_ref = func; // порядок: получаем ссылку на функцию int&& rrn = int(123); auto* pn = &rrn; // порядок: получаем адрес объекта auto& rn = rrn; // порядок: инициализируем lvalue-ссылку
- вызов функции или перегруженного оператора, возвращающего lvalue-ссылку, либо выражение преобразования к типу lvalue-ссылки;
- встроенные операторы присваивания, составные операторы присваивания ( = , += , /= и т. д.), встроенные преинкремент и предекремент ( ++a , —b ), встроенный оператор разыменования указателя ( *p );
- встроенный оператор обращения по индексу ( a[n] или n[a] ), когда один из операндов – lvalue массив;
- вызов функции или перегруженного оператора, возвращающего rvalue-ссылку на функцию;
- строковый литерал, например «Hello, world!» .
Строковый литерал отличается от всех остальных литералов в языке C++ именно тем, что является lvalue (хотя и неизменяемым). Например, можно получить его адрес:
auto* p = &”Hello, world!”; // тут константный указатель, на самом деле
prvalue
- все свойства rvalue (см. выше);
- не могут быть полиморфными: статический и динамический типы выражения всегда совпадают;
- не могут быть неполного типа (кроме типа void, об этом будет сказано ниже);
- не могут иметь абстрактный тип или быть массивом элементов абстрактного типа.
К категории prvalue относятся следующие выражения:
- литерал (кроме строкового), например 42 , true или nullptr ;
- вызов функции или перегруженного оператора, который возвращает не ссылку ( str.substr(1, 2) , str1 + str2 , it++ ) или выражение преобразования к нессылочному типу (например static_cast(x) , std::string<> , (int)42 );
- встроенные постинкремент и постдекремент ( a++ , b— ), встроенные математические операции ( a + b , a % b , a & b , a = b , и т.д.), встроенная операция взятия адреса ( &a );
- указатель this;
- элемент перечисления;
- нетиповой параметр шаблона, если он – не класс;
- лямбда-выражение, например [](int x) < return x * x; >.
xvalue
Примеры выражений категории xvalue:
- вызов функции или встроенного оператора, возвращающего rvalue-ссылку, например std::move(x);
и в самом деле, для результата вызова std::move() нельзя получить адрес в памяти или инициализировать им ссылку, но в то же время, это выражение может быть полиморфным:
struct XA < virtual void f() < std::cout >; struct XB : public XA < virtual void f() < std::cout >; XA&& xa = XB(); auto* p = &std::move(xa); // ошибка auto& r = std::move(xa); // ошибка std::move(xa).f(); // выведет “XB::f()”
- встроенный оператор обращения по индексу ( a[n] или n[a] ), когда один из операндов – rvalue-массив.
Некоторые особые случаи
Оператор запятая
Для встроенного оператора запятая (comma operator) категория выражения всегда соответствует категории выражения второго операнда.
int n = 0; auto* pn = &(1, n); // lvalue auto& rn = (1, n); // lvalue 1, n = 2; // lvalue auto* pt = &(1, int(123)); // ошибка, rvalue auto& rt = (1, int(123)); // ошибка, rvalue
Выражения типа void
Вызовы функций, возвращающих void, выражения преобразования типов к void, а также выбрасывания исключений (throw) считаются выражениями категории prvalue, но их нельзя использовать для инициализации ссылок или в качестве аргументов функций.
Тернарный оператор сравнения
Определение категории выражения a ? b : c – случай нетривиальный, всё зависит от категорий второго и третьего аргументов ( b и c ):
- если b или c имеют тип void, то категория и тип всего выражения соответствуют категории и типу другого аргумента. Если оба аргумента имеют тип void, то результат – prvalue типа void;
- если b и c являются glvalue одного типа, то и результат является glvalue этого же типа;
- в остальных случаях результат prvalue.
Для тернарного оператора определён целый ряд правил, по которым к аргументам b и c могут применяться неявные преобразования, но это несколько выходит за темы статьи, интересующимся рекомендую обратиться к разделу Стандарта Conditional operator [expr.cond].
int n = 1; int v = (1 > 2) ? throw 1 : n; // lvalue, т.к. throw имеет тип void, соответственно берём категорию n ((1 < 2) ? n : v) = 2; // тоже lvalue, выглядит странно, но работает ((1 < 2) ? n : int(123)) = 2; // так не получится, т.к. теперь всё выражение prvalue
Обращения к полям и методам классов и структур
Для выражений вида a.m и p->m (тут речь о встроенном операторе -> ) действуют следующие правила:
- если m – элемент перечисления или нестатический метод класса, то всё выражение считается prvalue (хотя ссылку таким выражением инициализировать не получится);
- если a – это rvalue, а m – нестатическое поле нессылочного типа, то всё выражение относится к категории xvalue;
- в остальных случаях это lvalue.
Для указателей на члены класса ( a.*mp и p->*mp ) правила похожие:
- если mp – это указатель на метод класса, то всё выражение считается prvalue;
- если a – это rvalue, а mp – указатель на поле данных, то всё выражение относится к xvalue;
- в остальных случаях это lvalue.
Битовые поля
Битовые поля – удобный инструмент для низкоуровнего программирования, однако, их реализация несколько выпадает из общей структуры категорий выражений. Например, обращение к битовому полю вроде бы является lvalue, т. к. может присутствовать в левой части оператора присваивания. В то же время, взять адрес битового поля или инициализировать им неконстантную ссылку не получится. Константную ссылку на битовое поле инициализировать можно, но при этом будет создана временная копия объекта:
struct BF < int f:3; >; BF b; b.f = 1; // OK auto* pb = &b.f; // ошибка auto& rb = b.f; // ошибка
Вместо заключения
Как я и упоминал во вступлении, приведённое описание не претендует на полноту, а лишь даёт общее представление о категориях выражений. Это представление позволит немного лучше понимать параграфы Стандарта и сообщения об ошибках компилятора.