Структура объединяет несколько связанных полей в одно значение пользовательского типа. Экземпляр struct можно присвоить переменной, передать методу, вернуть как результат и поместить в массив; во всех случаях копируется значение структуры. В пределах курса структура содержит только открытые поля, поэтому допустимое состояние не защищено самим типом и поддерживается внешними процедурными методами.
Копирование выполняется покомпонентно и рекурсивно по значимым полям. Числа, перечисления, вложенные структуры и другие значимые поля становятся независимыми копиями. Поле ссылочного типа передаёт только ссылку: две независимые структуры могут разделять одну строку или один массив, а изменение достигнутого массива остаётся наблюдаемым через каждую сохранившуюся ссылку.
Поведение зависит не только от типа, но и от категории выражения. Локальная переменная структуры, элемент массива структур, параметр по значению, параметр ref, выходной параметр out, параметр in и переменная foreach предоставляют разные возможности чтения и изменения. Контракт метода обязан фиксировать, изменяется ли исходное значение, возвращается ли новое, допускаются ли общие ссылочные поля и какие инварианты должны выполняться до и после вызова.
Опора — Раздел 20: собственный значимый тип struct. Дополнительно используются nullable-значимые типы из Раздела 4, способы передачи и кортежи из Раздела 13, массивы, поиск и процедурная сортировка из Раздела 15, копирование ссылок и глубина копии из Раздела 16, строки и порядковое сравнение из Раздела 19.
Каждая задача оформляется отдельной консольной программой полным шаблоном курса и компилируется с
<Nullable>enable</Nullable>. Разрешены только конструкции Разделов 1–20. Пользовательские структуры объявляются вне методов и содержат только учебныеpublic-поля. Пользовательские конструкторы, свойства, индексаторы, операторы, экземплярные методы,readonly struct,record struct, классы, интерфейсы, обобщения, делегаты, лямбда-выражения и обработка исключений запрещены. Все операции реализуются внешнимиstatic-методами классаProgram. Каждая модель имеет явный инвариант; значениеdefaultлибо признаётся допустимым, либо обрабатывается как отдельное недопустимое состояние. Для структуры со ссылочными полями отдельно задаются nullable-контракт, глубина копирования и допустимость общих объектов.
База практических заданий
Уровень I. Базовый
Название структуры, состав полей, сигнатуры методов, способ передачи и ожидаемая глубина копирования заданы условием. Требуется точно объявить тип, создать значения через
default,new T()и инициализаторы полей, проследить каждое копирование и подтвердить готовый контракт. Ученик не проектирует модель самостоятельно: сложность возрастает от простого значения к вложенным структурам, массивам структур, nullable-результатам, инвариантам и ссылочным полям.
1. Точка и значение по умолчанию: Объявить:
public struct Point2D
{
public double X;
public double Y;
}
Создать:
Point2D first = default;
Point2D second = new Point2D();
Point2D third = new Point2D
{
X = 3.5,
Y = -2.0
};
Вывести поля и подтвердить, что первые два значения равны (0, 0). Затем изменить только third.X и показать независимость остальных переменных. Объяснить, что в техническом контуре курса new Point2D() создаёт значение по умолчанию, а пользовательский конструктор не применяется.
2. Покомпонентная копия значимых полей: Объявить:
public struct Measurement
{
public int Id;
public double Value;
public bool IsValid;
public char UnitCode;
}
Создать исходное значение, скопировать его обычным присваиванием и изменить все поля копии. После каждого шага вывести обе структуры и составить таблицу состояния. Все поля являются значимыми, поэтому исходное значение остаётся неизменным.
3. Передача по значению, ref и out: Объявить:
public struct AccumulatorState
{
public long Sum;
public int Count;
}
Реализовать:
static void Initialize(
out AccumulatorState state)
static void AddByValue(
AccumulatorState state,
int value)
static void AddByReference(
ref AccumulatorState state,
int value)
Initialize создаёт нулевое состояние. Два метода добавления выполняют одинаковые изменения полей параметра. После каждого вызова вывести переменную вызывающего кода и объяснить, почему изменение параметра-копии исчезает, а ref изменяет исходное значение.
4. Чтение структуры через in: Объявить Vector2 с полями double X и double Y. Реализовать:
static double LengthSquared(in Vector2 vector)
Метод только читает поля и возвращает:
В отдельном некомпилируемом фрагменте попытаться присвоить новые значения vector.X, vector.Y и всему параметру. Объяснить, что in защищает переменную-параметр от присваивания, но не меняет природу самого типа.
5. Внешние операции над вектором: Для той же структуры реализовать чистые методы:
static Vector2 Add(
Vector2 left,
Vector2 right)
static Vector2 Scale(
Vector2 vector,
double factor)
static double Dot(
Vector2 left,
Vector2 right)
static double Length(Vector2 vector)
static bool AreClose(
Vector2 left,
Vector2 right,
double absoluteTolerance,
double relativeTolerance)
Исходные значения не изменяются. Проверить:
и
с комбинированным допуском.
6. Вложенные значимые поля: Объявить:
public struct Rectangle
{
public Point2D Min;
public Point2D Max;
}
Создать корректный прямоугольник, скопировать его присваиванием и изменить copy.Min.X и copy.Max.Y. Подтвердить, что обе вложенные точки скопированы как значения и исходный прямоугольник не изменился. Реализовать внешние методы Width, Height, Area и IsValid, где инвариант имеет вид:
7. Массив структур и значения по умолчанию: Объявить:
public struct Sample
{
public int Index;
public double Value;
public bool Accepted;
}
Создать массив длины 5, заполнить первые три элемента, а последние два оставить нетронутыми. Вывести все поля каждого элемента и объяснить происхождение значений по умолчанию. Затем вычислить количество, сумму, минимум, максимум и среднее только принятых образцов. Экстремумы инициализируются первым принятым элементом.
8. Замена параллельных массивов: Даны согласованные массивы:
int[] ids;
string[] names;
double[] scores;
Объявить:
public struct StudentRecord
{
public int Id;
public string Name;
public double Score;
}
Построить StudentRecord[] той же длины. После переноса изменить одну структуру в новом массиве и подтвердить, что исходные массивы не изменились. Сформулировать инвариант согласованности параллельных массивов и объяснить, какую связь между полями теперь выражает один элемент именованного типа.
9. Nullable-структура и Try...-контракт поиска: Объявить:
public struct Product
{
public int Code;
public string Name;
public decimal Price;
}
Реализовать:
static Product? FindByCode(
Product[] products,
int code)
static bool TryFindCheapest(
Product[] products,
out Product result)
FindByCode возвращает null при отсутствии товара. TryFindCheapest возвращает false и default для пустого массива; выходное значение используется только после проверки bool. При нескольких минимальных ценах сохраняется первый товар. Объяснить, почему Product? добавляет отдельное состояние отсутствия к пользовательскому значимому типу.
10. Устойчивая сортировка массива структур: Объявить:
public struct Observation
{
public int SourceId;
public double Value;
public int OriginalOrder;
}
Отсортировать массив устойчивой сортировкой вставками по неубыванию Value. При равных ключах элементы не сдвигаются:
previous.Value > current.Value
Подсчитать сравнения и записи целых структур. После сортировки проверить неубывание значений и возрастание OriginalOrder внутри групп равных ключей.
11. Каноническая дробь: Объявить:
public struct Fraction
{
public long Numerator;
public long Denominator;
}
Реализовать:
static bool TryCreateFraction(
long numerator,
long denominator,
out Fraction result)
Предусловие: аргументы не равны long.MinValue. Нулевой знаменатель даёт false и default. Успешный результат:
- сокращён;
- имеет положительный знаменатель;
- представляет ноль как
0/1.
Отдельный метод IsCanonical проверяет все инварианты, не изменяя значение.
12. Структура со ссылочным полем: Объявить:
public struct Packet
{
public int Id;
public string Label;
public int[] Data;
}
Создать значение и копию присваиванием. Затем последовательно:
- изменить
copy.Id; - присвоить
copy.Labelновую строку; - изменить
copy.Data[0]; - заменить
copy.Dataновым массивом.
После каждого шага вывести обе структуры и ReferenceEquals для Label и Data. Объяснить независимость значимого поля, неизменяемость строки, общность исходного массива и разрыв этой общности после замены ссылки.
Уровень II. Продвинутый
Условие описывает предметную модель и требуемое поведение, но не готовый тип, поля, инвариант и процедурный API. Требуется самостоятельно выбрать между отдельными переменными, кортежем и
struct, определить состав значения, способы возврата и глубину копирования и применить массив структур в содержательном алгоритме. В отличие от Уровня I ученик проектирует модель данных и границы её допустимых состояний.
1. Именованный тип и локальный пакет результатов: Спроектировать Point2D для координаты, которая хранится в массиве и передаётся между несколькими методами. Метод анализа массива точек возвращает именованный кортеж:
(
Point2D min,
Point2D max,
Point2D centroid)
Письменно обосновать: точка имеет самостоятельный предметный смысл и долгий срок жизни, а три результата анализа образуют локальный пакет одного вызова и не требуют отдельного типа.
2. Реестр результатов: Предметная запись содержит идентификатор, имя, три оценки и признак допуска. Самостоятельно спроектировать ExamRecord, заменить параллельные массивы массивом структур и реализовать:
- проверку уникальности идентификаторов и допустимости оценок;
- вычисление итогового балла;
- поиск записи по идентификатору;
- отбор допущенных записей в новый массив точного размера;
- вычисление среднего итогового балла допущенных участников.
Вычисляющие методы не выполняют ввод и вывод.
3. Замкнутый интервал: Спроектировать Interval для вещественного отрезка с инвариантом:
Реализовать TryCreate, Length, Contains, Intersects и TryIntersect. Пересечение либо возвращает корректный Interval, либо сообщает отсутствие через bool и out. Значение default(Interval) интерпретируется как допустимый вырожденный интервал [0,0]; это решение должно быть явно закреплено контрактом.
4. Геометрическая модель вектора: Самостоятельно спроектировать Vector2 и внешние методы:
- норма;
- нормализация;
- проекция на ненулевой ;
- отражение относительно прямой с единичным направляющим вектором;
- приближённое сравнение.
Недопустимые NaN, бесконечности, отрицательный допуск и нулевая норма отвергаются через bool. Для успешной проекции проверить ортогональность:
5. Арифметика рациональных чисел: Расширить Fraction внешними методами сложения, вычитания, умножения, сравнения и преобразования в double. Каждый результат нормализуется. Перед умножением выполнять перекрёстное сокращение; контрольные данные гарантируют представимость оставшихся операций в long. Сравнение также должно уменьшать множители до перекрёстного умножения. Каждый метод подтверждает каноничность результата.
6. Аффинное преобразование плоскости: Спроектировать:
public struct Transform2D
{
public double M11;
public double M12;
public double M21;
public double M22;
public double OffsetX;
public double OffsetY;
}
Внешними методами реализовать:
- тождественное преобразование;
- перенос;
- масштабирование;
- применение к
Point2D; - композицию двух преобразований;
- определитель линейной части;
TryInverse.
Обратное преобразование допустимо, если определитель конечен и по модулю больше заданного допуска. Проверить:
7. Стабильная сортировка по нескольким полям: Спроектировать Candidate с полями имени, балла, штрафа и исходного номера. Ручной устойчивой сортировкой вставками упорядочить массив:
- больший балл раньше;
- при равенстве меньший штраф раньше;
- затем имя по
StringComparison.Ordinal; - при полном равенстве сохранить исходный порядок.
Отдельный чистый метод ComesBefore задаёт отношение порядка.
8. Снимок структуры со ссылочным полем: Спроектировать Batch с номером версии, строковой меткой и массивом измерений. Реализовать:
ShallowCopyобычным присваиванием;Snapshot, создающий независимую копию массива;AreEqualByValue, сравнивающий все поля и элементы массива.
Строковая ссылка может оставаться общей из-за неизменяемости string. Серией изменений доказать различие идентичности, поверхностной копии и содержательного равенства.
9. Разреженный многочлен: Объявить:
public struct Term
{
public int Degree;
public double Coefficient;
}
Многочлен представлен массивом членов, упорядоченным по убыванию степени, без повторяющихся степеней и коэффициентов, модуль которых не превосходит заданный нулевой допуск. Спроектировать:
- проверку инварианта;
- вычисление значения схемой Горнера с учётом пропущенных степеней;
- производную;
- сложение двух многочленов слиянием за .
Результат сложения не содержит близких к нулю членов.
10. Разреженный вектор: Спроектировать:
public struct SparseVector
{
public int Dimension;
public int[] Indices;
public double[] Values;
}
Инвариант:
- массивы существуют;
- длины совпадают;
Dimension >= 0;- индексы строго возрастают;
- каждый индекс лежит в
[0, Dimension); - значения конечны и не равны нулю в пределах допуска.
Реализовать проверку, глубокий снимок, скалярное произведение с плотным массивом и сложение двух корректных разреженных векторов слиянием без коллекций.
11. Пакетное изменение товарных остатков: Спроектировать структуры StockItem и StockChange. Реализовать:
static bool TryApplyChanges(
StockItem[] source,
StockChange[] changes,
out StockItem[] result)
Требования:
- коды товаров в
sourceуникальны; - каждый код изменения существует;
- повторные изменения одного кода разрешены;
- итоговое количество каждого товара неотрицательно и представимо в
int; - при отказе
sourceне изменяется, аresultпуст; - при успехе возвращается новый массив, не разделяющий изменяемых ссылочных полей с источником.
Проверка всех итоговых состояний завершается до публикации результата.
12. Система материальных точек: Самостоятельно спроектировать структуру материальной точки: идентификатор, метка, координаты и положительная масса. Для массива точек реализовать:
- проверку допустимости и уникальности идентификаторов;
- общую массу;
- центр масс;
- момент инерции относительно центра;
- поиск наиболее удалённой точки;
- устойчивую сортировку по квадрату расстояния до центра без
Math.Sqrt.
Все вычисляющие методы отделены от вывода; результаты проверяются независимыми балансными равенствами.
Уровень III. Экспертный
Исследуется не предметная модель, а точная семантика значения структуры и категория выражения, через которое к нему обращаются. До сборки и запуска требуется предсказать определённость полей, изменения исходного значения, связи ссылочных объектов, nullable-диагностику и допустимость операции; после — объяснить результат через покомпонентное копирование, значение по умолчанию, псевдонимы
refи отсутствие встроенной защиты инварианта.
1. Значение по умолчанию против инварианта: Исследовать:
default(Fraction);new Fraction();- элемент
new Fraction[3]; - значение, созданное через
TryCreateFraction.
До запуска предсказать числитель, знаменатель и результат IsCanonical. Показать, что тип допускает состояние 0/0, хотя процедурный фабричный метод никогда его не создаёт. Сравнить три политики API:
- проверять каждое входное значение;
- считать каноничность предусловием;
- хранить дополнительный признак состояния.
Объяснить, почему открытые поля и значение по умолчанию не позволяют типу самостоятельно запретить некорректное состояние.
2. Значение по умолчанию и ссылочные поля: Объявить:
public struct LabelledBuffer
{
public int Id;
public string Name;
public int[] Data;
}
Исследовать default(LabelledBuffer), new LabelledBuffer() и элемент массива структур. До разыменования проверить Name is null и Data is null, записать nullable-предупреждения компилятора и фактические значения. Аварийное чтение Data.Length выполнить отдельно. Объяснить, что nullable-аннотация не изменяет представление времени выполнения: ссылочные поля значения по умолчанию содержат null.
3. Определённое присваивание полей локальной структуры: Для:
Point2D point;
в отдельных фрагментах исследовать:
point.X = 1.0;
Console.WriteLine(point.X);
Console.WriteLine(point.Y);
PrintPoint(point);
point.Y = 2.0;
PrintPoint(point);
До сборки классифицировать каждое чтение и передачу целого значения. Объяснить различие между определённостью отдельного поля и определённостью всей локальной переменной структуры.
4. Вложенное копирование и общий объект: Объявить Payload с числовым кодом и массивом данных и Envelope с идентификатором, строковой меткой и полем Payload. Создать Envelope original, скопировать его и изменить в копии:
- идентификатор внешней структуры;
- код вложенной структуры;
- строковую метку;
- элемент массива;
- ссылку на массив.
До запуска нарисовать граф значений и объектов после каждого шага. Объяснить, почему обе структуры и вложенный Payload скопированы, а объект массива первоначально остаётся общим.
5. Элемент массива, локальная копия, foreach и ref: Для Point2D[] points сравнить:
Point2D copy = points[0]; copy.X = ...;points[0].X = ...;- попытку изменить поле переменной
foreach; - передачу
points[index]в метод черезref.
До запуска предсказать итоговый массив и диагностику некомпилируемого фрагмента. Объяснить, почему элемент массива является изменяемой переменной, локальная и foreach-переменная содержат копию, а ref предоставляет доступ к исходной ячейке.
6. in не означает глубокую неизменяемость: Объявить структуру с числовым полем и полем-массивом. Передать её методу через in. Внутри:
- прочитать оба поля;
- изменить элемент массива;
- попытаться переприсвоить числовое поле;
- попытаться переприсвоить ссылочное поле;
- попытаться присвоить новый объект всему параметру.
До сборки классифицировать действия. После запуска объяснить, что in запрещает изменять значение параметра, но не делает неизменяемым объект, достижимый через скопированную ссылку.
7. Два ref-параметра как псевдонимы одной переменной: Объявить:
public struct Account
{
public int Id;
public decimal Balance;
}
Реализовать:
static bool TryTransfer(
ref Account source,
ref Account target,
decimal amount)
Метод проверяет положительную сумму и достаточность средств, затем уменьшает source.Balance и увеличивает target.Balance. Вызвать его для двух разных переменных и для одной переменной, переданной в оба параметра. До запуска предсказать последовательность состояний. Объяснить, почему типы и модификаторы не гарантируют различие мест хранения и почему контракт обязан запрещать совпадение аргументов либо учитывать его семантически.
8. Сортировка структур не разрывает общие ссылки: Объявить Record с ключом и массивом полезной нагрузки. Создать несколько структур:
- две разделяют один массив;
- третья содержит отдельный массив с тем же содержимым;
- четвёртая содержит иной массив.
Создать поэлементную копию массива структур, отсортировать копию вставками по ключу и изменить полезную нагрузку через один отсортированный элемент. До запуска предсказать порядок, содержимое и все отношения ReferenceEquals. Затем построить глубокую копию каждого элемента и сравнить поведение.
9. Идентичность, поверхностное и содержательное равенство: Для структуры:
public struct Packet
{
public int Id;
public int[] Data;
}
в отдельном фрагменте попытаться применить left == right и записать диагностику компилятора. Затем реализовать два внешних отношения:
static bool AreShallowEqual(
Packet left,
Packet right)
static bool AreEqualByContent(
Packet left,
Packet right)
Первое требует равенства Id и идентичности массивов. Второе требует равенства Id, длины и всех элементов. Построить пары:
- две копии одной структуры;
- разные массивы с одинаковыми элементами;
- общий массив при разных
Id; - разные элементы.
Объяснить, почему семантика значимого типа определяет копирование, но не предоставляет автоматически предметный оператор равенства.
Обязательные контрольные наборы
| Задача | Исходные данные |
|---|---|
| I.1 | default(Point2D), new Point2D(), точка (3.5,-2) |
| I.2 | (Id,Value,IsValid,UnitCode) = (7,12.5,true,'V') |
| I.3 | начальное состояние (Sum,Count) = (10,2), добавляемое значение 7 |
| I.4 | вектор (3,4) |
| I.5 | u = (3,4), v = (-2,5), w = (1,-3), factor = -0.5 |
| I.6 | прямоугольник с углами (-2,1) и (5,6); недопустимый порядок углов |
| I.7 | три образца (0,2.5,true), (1,-1,false), (2,4.5,true) и два значения по умолчанию |
| I.8 | ids = [10,20,30], names = ["A","B","C"], scores = [81.5,92,81.5] |
| I.9 | товары с кодами [20,10,30,40] и ценами [5.5m,2m,2m,9m]; цели 30 и 99; пустой массив |
| I.10 | значения [3,1,3,2,1] с исходными номерами [0,1,2,3,4] |
| I.11 | (2,4), (-6,-8), (0,5), (5,0) |
| I.12 | пакет (7,"alpha",[1,2,3]) |
| II.1 | точки (0,0), (2,4), (-1,5), (3,-2) |
| II.2 | пять записей, включая повтор идентификатора, недопуск и граничные оценки |
| II.3 | [1,5], [3,8], [6,9], [5,5], недопустимая пара (4,2) |
| II.4 | u = (3,4), v = (2,0), нулевой вектор и NaN |
| II.5 | 1/2, -3/4, 5/6, 0/7 |
| II.6 | тождественное преобразование, перенос (3,-2), масштабы (2,0.5), обратимое и вырожденное преобразования |
| II.7 | кандидаты с совпадающими баллами, штрафами и именами, но разными исходными номерами |
| II.8 | пакет версии 3, метка "batch", измерения [4,8,15,16] |
| II.9 | 3x^5-2x^2+1 и -3x^5+4x^3+2x^2-7 |
| II.10 | размерность 10, пары (1,2.5), (4,-1), (9,3); второй вектор с пересекающимися индексами |
| II.11 | товары (10,5), (20,0), (30,12); повторные изменения, неизвестный код и уход количества ниже нуля |
| II.12 | массы 2, 3, 5 в координатах (0,0), (4,0), (0,6) |
| III.1 | все способы получения значения Fraction из условия |
| III.2 | default, new T() и массив LabelledBuffer[2] |
| III.3 | все фрагменты определённого присваивания из условия |
| III.4 | конверт с кодом 5, меткой "source" и данными [10,20,30] |
| III.5 | точки [(1,2),(3,4),(-1,5)] |
| III.6 | структура (Version = 2, Data = [1,2,3]) |
| III.7 | счета (1,100m) и (2,40m), перевод 25m; один счёт в обоих параметрах |
| III.8 | ключи [3,1,2,1]; две структуры разделяют [7,8], третья содержит отдельный [7,8] |
| III.9 | пары из условия с общими и независимыми массивами |
Итог модуля
Ученик объявляет простой пользовательский struct, создаёт значение через default, new T() и инициализатор полей и прослеживает покомпонентное копирование вложенных значимых и ссылочных полей. Он различает локальную переменную, элемент массива, параметр по значению, ref, out, in и переменную foreach, использует nullable-структуру для представления отсутствия результата и учитывает возможность псевдонимов между ref-параметрами. Именованный тип выбирается для устойчивой предметной модели, а кортеж — для локального пакета результатов; массив структур заменяет параллельные массивы и участвует в поиске, фильтрации, устойчивой сортировке и численных алгоритмах. Допустимость дроби, интервала, вектора, преобразования, разреженного представления и записи со ссылочными полями обеспечивается внешним процедурным API, явной глубиной копирования и проверкой инварианта на каждой границе.
Покрытие опоры и границы
- Объявление
struct, открытые поля, создание черезdefault,new T()и инициализатор: I.1–I.2, III.1–III.3. - Покомпонентное копирование значимых полей: I.2, I.6, III.3–III.5.
- Вложенные структуры: I.6, II.6, II.12, III.4.
- Передача по значению,
ref,outиin: I.3–I.5, III.5–III.7. - Nullable-пользовательский значимый тип: I.9.
- Значение по умолчанию и нарушение предметного инварианта: I.1, I.7, I.11, II.3, III.1–III.3.
- Массив структур, значения элементов по умолчанию и изменение через индекс: I.7–I.10, II.2, II.7, II.9–II.12, III.5 и III.8.
- Замена параллельных массивов именованным типом: I.8, II.2.
- Выбор между отдельными переменными, кортежем и
struct: II.1–II.3. - Поиск, фильтрация и процедурная сортировка по полям: I.9–I.10, II.2, II.7, II.12.
- Внешний процедурный API и проверка инвариантов: I.5–I.11, II.2–II.12.
- Дробь, интервал, вектор и аффинное преобразование: I.5–I.6, I.11, II.3–II.6.
- Разреженные модели и слияние массивов структур: II.9–II.10.
- Ссылочные поля, общие объекты, поверхностная и глубокая копии: I.12, II.8, II.10–II.11, III.2, III.4, III.6, III.8–III.9.
- Псевдонимы
refи совпадение мест хранения: III.7. - Поверхностное и содержательное равенство: II.8, III.9.
За границей: инкапсуляция, свойства, пользовательские конструкторы, экземплярные методы, операторы, readonly struct, record struct, интерфейсы равенства, обобщения и объектно-ориентированная модель относятся к Курсу II. Упаковка и распаковка, внутреннее устройство System.ValueType, физическое размещение значения и оптимизации копирования не используются как правило семантики этого модуля. Структура не считается автоматически неизменяемой, маленькой или дешёвой: вывод делается по составу полей, контракту передачи и реально выполняемым копированиям.

