Большинство гайдов в Интернете или давно утратили актуальность, или содержат лишь небольшие вкрапления новых возможностей, но лишены последовательности. Есть и другая крайность - ИИ простыни сгенерированного текста под видом статей, которые очень тяжело читать. Я хочу сделать свою попытку изменить ситуацию.
0. Предисловие
Зачем вам учить C# в 2026 году
Язык C# (читается как си-шарп) является современным языком программирования, позволяющим вам разрабатывать программы, мобильные приложения, веб-сайты и игры. Да, думаю, что большинство из вас хотело бы создавать свои игры на Unity или Godot! Конечно же, чтобы писать игры и программы, сначала нужно разобраться с тем КАК это делать.
На данный момент C# уже во многих аспектах превосходит своих прародителей - Java и Delphi, первого в производительности, а второго в современных средствах разработки. Вы можете достичь высокой скорости, чем редко может похвастаться Python, в то же время от "сложных" языков, вроде C++ и Rust, C# отличается автоматической сборкой мусора, вместо ручного управления памятью, а также возможность концентрироваться на абстракциях, а не технических деталях.
Немного об этом руководстве
Это руководство для начинающих программистов и junior-разработчиков, которые хотят понимать современный C#. Да, мы будем рассматривать именно современный C#, что сразу снимет с меня необходимость описывать многие устаревшие синтаксические конструкции. Я специально не буду фокусироваться на тонкостях, вроде всех способов вывода текста в консоль, просто потому что это никому не интересно. Не буду привязывать читателя к графическим фреймворкам, чтобы повествование было универсальным. По возможности буду приводить аналогии из игр, чтобы материал был понятнее.
Что потребуется
Среда разработки Visual Studio 2022+ или JetBrains Rider. Допустимо поставить VS Code, но новичкам в нем будет сложнее. Наконец, в крайнем случае используйте SharpLab или любую другую интерактивную онлайн IDE, но комфорт будет в разы меньше.
Готовность вводить код из примеров.
Желание.
Часть 1. Основы
1. Синтаксис и особенности
Синтаксически C# является си-подобным, поэтому многие синтаксические конструкции здесь похожи на аналогичные в C, C++, Java и подобных им языках.
Рассмотрим типичный Hello World. Чтобы его создать, нужно в среде разработки создать консольное приложение (в Visual Studio - консольное приложение Microsoft) и убрать галочку с "Use top level statements", а версию языка поставить .NET 9 или выше.
Свою нелюбовь к top level statements я объясню очень просто: хочу, чтобы вы привыкали видеть все детали вашего кода. Вот так бы выглядел код, если бы мы оставили эту галочку:
Console.WriteLine("Hello World!");
Этого крайне мало для понимаю, особенно, если вы будете изучать чужие репозитории и статьи по C#.. А как только мы пойдем дальше, и нам потребуется все, что от нас скрыли, случится культурный шок.
Возвращаемся к созданию консольного приложения. После нажатия на кнопку создания, вы увидите приблизительно такую картину.
using System;
namespace MyProgram
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
Предлагаю этот код немного сократить (можете просто вставить код из этого фрагмента):
using System;
class Program
{
static void Main()
{
Console.WriteLine("Hello World!");
}
}
Из примера можно сделать следующие выводы:
Программа - это тоже класс
Точка входа в программу - метод Main
Блоки разделяются фигурными скобками
Вызовы и операции заканчиваются точкой с запятой
?Важно: C# чувствителен к регистру. Это значит, что Console и console - два разных идентификатора, поэтому будьте внимательны!
Верхняя строчка "using System;" означает, что мы хотим использовать типы, доступные в пространстве имен (namespaces) System. Этого делать необязательно, но тогда нам придется указывать полные имена типов, например, System.DateTime, вместо DateTime.
Рассмотрим простую программу "поздороваться с пользователем":
using System;
class Program
{
static void Main()
{
Console.WriteLine("Как тебя зовут?");
string name = Console.ReadLine(); // записать ввод пользователя
Console.WriteLine("Привет, " + name);
}
}
При запуске код выведет текст ("как тебя зовут?") и будет ожидать ввода текста от пользователя и нажатия Enter. После чего поздоровается с пользователем.
Через класс Console мы общаемся с внешним миром:
ReadLine - ввод текста пользователем
WriteLine - вывод текста в консоль отдельной строкой
Write - вывод текст в консоль
Первый способ вывода текста будем использовать чаще всего для простоты.
Два слеша (//) означают комментарий, то есть, это подсказка программисту, которая никак не влияет на программу. Что-то вроде заметок на полях в тетради.
Что такое string, и как именно мы записали имя пользователя, будет раскрываться в дальнейших разделах.
Важно заметить, что весь дальнейший код нужно писать как раз внутри метода Main:
static void Main()
{
// ваш код будет здесь
}
Именно с запуска этого метода начинается каждая программа. Это позволит мне давать информацию последовательно, чтобы не перегружать руководство деталями.
2. Примитивные типы
Являются минимальными строительными кирпичиками в более сложных типах. Например, рассмотрим простую структуру точки в двухмерном пространстве:
struct Point
{
public int X;
public int Y;
}
Структура объединяет несколько значений в один тип.
Значимые (скалярные) типы
Значимые типы являются структурами (struct), следовательно передаются по значению, а не ссылке. Значением по умолчанию для значимых типов является default.
Логический тип
Тип bool (System.Boolean) может иметь одно из двух значений: true (истина) или false (ложь). Это объясняется использованием его в качестве результата в условиях, например:
5 < 10 // истина, потому что 5 меньше 10
-1 > 0 // ложь, потому что -1 меньше 0
Например, условие, что персонаж мертв можно описать так: его здоровье меньше единицы.
hero.Health < 1
Если оно истинно, то он мертв, если ложно - жив, все логично.
Целые числа
Целые числа - это те числа, которые не имеют дробной части. Например, число 12 - целое, а вот число 1.2 - нет. Для чего можно использовать целые числа: количество чего-либо (7 патронов в обойме), номер игрока или команды, количество побед подряд и т.д.
Ключевое слово |
Тип |
Размер (байт) |
Диапазон значений |
byte |
System.Byte |
1 |
От 0 до 255 |
sbyte |
System.SByte |
1 |
От -128 до 127 |
short |
System.Int16 |
2 |
От -32 768 до 32 767 |
ushort |
System.UInt16 |
2 |
От 0 до 65 535 |
int |
System.Int32 |
4 |
От -2 147 483 648 до 2 147 483 647 |
uint |
System.UInt32 |
4 |
От 0 до 4 294 967 295 |
long |
System.Int64 |
8 |
От -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807 |
ulong |
System.UInt64 |
8 |
От 0 до 18 446 744 073 709 551 615 |
nint |
System.IntPtr |
4-8 |
Аналогично int для 32-битных систем, long - для 64-битных |
nuint |
System.UIntPtr |
4-8 |
Аналогично uint для 32-битных систем, ulong - для 64-битных |
?Чтобы не запутаться
Если тип изначально знаковый (позволяет отрицательные значения), например int, то его альтернатива будет иметь префикс u (unsigned), как uint. Если же тип изначально беззнаковый, как byte, то его альтернатива будет иметь префикс s (signed), как sbyte.
?Почему их так много?
Байты используются при передаче информации в потоках (stream), например, можно прочитать файл побайтово. Чаще всего используют int, который является стандартом в большинстве языков. Тип long используют для хранения особенно больших значений, например, даты. Наконец, nint, как платформозависимое число, используется для обращений к системным API.
Запись чисел в других системах счисления
Мы уже научились записывать числа в привычной десятичной (decimal) системе счисления.
int dec = 1024;
Но при желании мы можем писать в еще двух, используя специальные префиксы:
Для шестнадцатеричной (hexadecimal) системы используется префикс 0x:
int hex = 0xff; // 256
В этой системе можно использовать латинские буквы от A до F включительно.
Для двоичной (binary) системы используется префикс 0b:
int bin = 0b1011; // 11
Для вашего удобства, разряды можно отделять знаком подчеркивания (_), что особенно полезно для наглядности в двоичной системе счисления:
int largeBinNum = 0b1111_0110_1111; // 3951
Числа с плавающей запятой
Они же вещественные числа. А числами с плавающей запятой их называют из-за особенностей их реализации в электронике: в отличии от чисел реального мира, где память ничем не ограничена, здесь у нас есть жесткие границы памяти, поэтому и хранить такие числа без потерь в точности нельзя. Для этого используются математические формулы, которые, тем не менее, могут вести к потере точности после определенного количества знаков.
Вещественные числа используются там, где целых не хватает. Например, в Dota 2 многие характеристики, в том числе регенерация здоровья или маны, являются именно вещественными числами.
Ключевое слово |
Тип |
Размер (байт) |
Точность |
- |
System.Half |
2 |
~3-4 цифры |
float |
System.Single |
4 |
~6-9 цифр |
double |
System.Double |
8 |
~5-17 цифр |
decimal |
System.Decimal |
16 |
28-29 цифр |
Half появился недавно и не имеет ключевого слова, в отличии от других типов.
Почему их так много?
Half чаще всего используется в графике и ML для экономии памяти, в обычных программах почти не применяется. Для быстрых графических вычислений (матрицы, вектора), в том числе в Unity, используют float, потому что он требует мало памяти. Для более точных математических вычислений используют double. А decimal используют в денежных вычислениях, где особенно важно сохранять правильную точность дробной части числа.
Символьный тип
Тип char (System.Char) используется для представления символов, из которых состоят строки текста. Отличается тем, что требует указывать значение в одинарных кавычках, которые называются апострофами (').
char x = 'x';
char breakLine = '\n';
Во втором случае используется спецсимвол разрыва строки, но это все еще один символ.
C# по умолчанию использует кодировку Unicode (UTF-16), поэтому вы можете использовать русский язык без дополнительный усилий (в отличии от C и C++).
?А что с эмодзи?
Неожиданным поведением может показаться размер эмодзи. Так, для них нужно использовать строки, а не символы, потому что они могут состоять из нескольких юникод символов, а значит их реальный размер не вписывается в char.
Оператор sizeof
Самостоятельно узнать размер примитивного типа можно оператором sizeof, например:
int sizeInt = sizeof(int); // 4
int sizeDouble = sizeof(double); // 8
Но про переменные будет ниже. А для непримитивных типов потребуется небезопасный (unsafe) контекст, но этого мы рассматривать не будем. К счастью, C# позволяет не думать о таких деталях в большинстве случаев!
Ссылочные типы
Ссылочные типы являются классами (class) и передаются по ссылке, что и следует из их названия. Значение по умолчанию для ссылочных типов является null (проще говоря - отсутствие значения). Если попытаться обратиться к переменной, имеющей значение null, то это приведет к исключению (ошибке) - ⛔NullReferenceException.
Тип Object
Тип object (System.Object) отличается тем, что может хранить любое значение. Это является следствием того, что все другие типы наследуются от object.
object str = "Это строка";
object fact = true;
Но здесь нужно помнить о ссылочной природе object: если в него записать значение значимого типа, то это приведет к дополнительным расходам на так называемую "упаковку". Пока можете не думать об этом.
Строковый тип
Тип string (System.String) представляет строки, то есть текст. Как мы уже говорили, строки указываются внутри двойных кавычек.
string bio = "Меня зовут Вася, мне 25 лет";
В строках можно использовать спецсимволы, как и в символах:
string breakingStr = "Абзац 1\nАбзац 2";
Если же нам нужно отобразить обратный слеш или спецсимвол, нужно его экранировать еще одним слешем:
string UnbreakingStr = "Абзац 1\\nАбзац 2";
Или использовать необработанные (или сырые) строки:
string rawBreakingStr = @"Абзац 1\nАбзац 2";
В такой строке будут игнорироваться все спецсимволы. Это особенно удобно для путей к файлам, потому что они содержат слеш.
?Задание: самостоятельно создать экземпляры следующих типов: строка, логическое, байт, символ, любое вещественное.
3. Переменные
Проще всего думать о переменных, как о коробках, где хранятся какие-то значения. А имена переменных - названиях этих самых коробок. Переменная может хранить значение только того типа, который соответствует ее значению, поэтому если переменная имеет тип int, то и положить в него можно только целое число этого типа.
Синтаксис переменных
тип имяПеременной = значение;
При этом переменным принято давать имена с маленькой буквы, чтобы не путать их с имена методов или свойств.
Например:
int age = 25;
Некоторые типы требуют указания литералов. Например:
float weight = 65.7; // ошибка потому, что число имеет тип double
Но стоит использовать литерал f, и мы добьемся нужного поведения.
float weight = 65.7f; // литерал f для типа float
Аналогичные литералы есть и для других типов:
m - decimal
l - long
ul - ulong
Строки и символы не требуют литералов, потому что кавычки и апострофы четко определяют тип значения.
Для не примитивных типов требуется ключевое new, которое означает вызов конструктора.
DateTime someDate = new DateTime(1900, 1, 1); // 01.01.1900
Использовать переменную, которой не присвоено значение нельзя:
int x;
Console.WriteLine(x); // на этой строчке подсветится ошибка
А еще в C# можно указывать несколько переменных одного типа через запятую.
Объявление:
float x, y, z;
Или сразу с инициализацией:
float x = 5, y = 2, z = 0;
Вывод типа
Иногда название типа может занимать много места, поэтому придумали оператор var, который сам подставляет название типа в зависимости от значения справа (после знака =).
var pi = 3.1415; // pi имеет тип double
var myStr = "Some text"; // myStr имеет тип string
Важно отметить, что var ничего не меняет в переменной и не делает ее динамичной, он просто позволяет не делать этого явно. Отсюда и название "вывод типа" (type inference).
?Задание: поэкспериментируйте с литералами и выводом типа. Посмотрите на подсказки вашей среды разработки при наведении курсора на имени переменной.
4. Операторы и операции
Целые числа
Для вашей простоты я пишу x, если операция производится над одним операндом (т.е. она унарная), или x и y, если она производится над двумя операндами (т.е. бинарная).
Операция |
Название |
Результат |
Арифметические | ||
x++ |
Инкремент (увеличить на 1) |
Целое число |
x-- |
Декремент (уменьшить на 1) |
Целое число |
+x |
Унарный плюс |
Целое число |
-x |
Унарный минус |
Целое число |
x+y |
Сложение |
Целое число |
x-y |
Вычитание |
Целое число |
x*y |
Умножение |
Целое число |
x/y |
Деление |
Целое число |
x%y |
Деление по модулю (остаток от деления) |
Целое число |
Логические | ||
x==y |
Равенство |
Логический |
x!=y |
Неравенство |
Логический |
x<y |
Меньше |
Логический |
x>y |
Больше |
Логический |
x<=y |
Меньше или равно |
Логический |
x>=y |
Больше или равно |
Логический |
Битовые | ||
x<<y |
Сдвиг влево на y бит |
Целое число |
x>>y |
Сдвиг вправо на y бит |
Целое число |
~x |
Битовое НЕ |
Целое число |
x&y |
Битовое И |
Целое число |
x|y |
Битовое ИЛИ |
Целое число |
x^y |
Исключающее ИЛИ |
Целое число |
Кстати, некоторые операции можно записывать короче:
x = x + 1;
Можно заменить на
x += 1;
Или на инкремент:
x++;
?Как работают битовые операции?
Дело в том, что каждое число имеет свое двоичное представление в памяти.
Сдвигая число на X бит влево мы умножаем на 2 в степени X.
Сдвигая число на X бит вправо мы делим его на 2 в степени X.
int myNum = 25; // В двоичном виде 11001
int myOtherNum = 3; // В двоичном виде 00011
int xor = myNum ^ myOtherNum; // 11001 ^ 00011
Console.WriteLine(xor); // 26 а в двоичном виде 11010
Числа с плавающей запятой
Над ними разрешены те же операции, за исключением битовых, а так же деления по модулю. Результатом арифметических операций будет вещественное число.
А что будет если в операции участвует как целое, так и вещественное число? Результатом будет вещественное число.
var a = 10 / 3; // 3
var b = 10 / 3f; // 3.33...
Еще одна особенность вещественных чисел - особые значения.
NaN - не число (Not a Number)
Infinity - бесконечность (Positive Infinity)
-Infinity - минус бесконечность (Negative Infinity)
?Такие необычные значения можно получить, например, при делении вещественного числа на 0 - так вы получите бесконечность (Inf). Это может показаться странным, но таков стандарт (IEEE-754) чисел с плавающей запятой, используемый во многих языках программирования.
Строки
Операция |
Название |
Результат |
x==y |
Равенство |
Логический |
x!=y |
Неравенство |
Логический |
x+y |
Конкатенация (concat) |
Новая строка |
x[y] |
Индекс |
Символ |
x[y..z] |
Срез (slice) |
Новая строка |
Интересно то, что строку допустимо складывать с числами, результатом всегда будет строка.
string str = "10" + 10; // 1010
Обратите внимание, что строки в C# не могут меняться, поэтому каждый раз мы будем получать новую строку! Сейчас это знание может показаться бесполезным, но в нагруженных программах это может серьезно повлиять на нагрузку системы.
Логический тип
Операция |
Название |
Результат |
x==y |
Равенство |
Логический |
x!=y |
Неравенство |
Логический |
x&&y |
И |
Логический |
x||y |
ИЛИ |
Логический |
!x |
НЕ |
Логический |
Приоритет операций
Важно заметить, что существуют приоритеты операций, причем она такие же, как и в математике.
int n = 2 + 2 * 2; // 6
То же самое распространяется и на скобки, которые можно использовать при желании.
int n = (2 + 2) * 2; // 8
?Задание: вывести в консоль результат деления 25 на 3 и остаток от этого деления.
5. Условия
if-else
Как я уже писал, условия могут быть истинными (true) или ложными (false).
if (условие)
{
// действие, если условие верно
}
Соответственно, если результат условия true, то действия в блоке будут выполнены. Если нет, не будут.
Комбинируя если-иначе, мы можем описать действия, которые будут выполнены, если условие ложно:
if (условие)
{
// действие, если условие верно
}
else
{
// действие, если условие неверно
}
Ну и главное, что мы можем комбинировать else и if, чтобы описать сколько угодно блоков условий:
if (условие 1)
{
// действие, если условие 1 верно
}
else if (условие 2)
{
// действие, если условие 2 неверно
}
// другие else if или else по желанию...
Рассмотрим пример с длинным ветвлением, где будем сравнивать температуру:
int t = 25;
if (t < 0)
{
Console.WriteLine("Очень холодно");
}
else if (t < 10)
{
Console.WriteLine("Холодно");
}
else if (t < 20)
{
Console.WriteLine("Прохладно");
}
else if (t < 30)
{
Console.WriteLine("Тепло");
}
else
{
Console.WriteLine("Жарко");
}
Конечно же, вы можете написать if в одну строку, если вы хотите сделать всего одно действие после условия, например:
if (t > 50) Console.WriteLine("Слишком большое значение!");
?Однако, не стоит слишком злоупотреблять однострочными if, потому что они уменьшают читабельность кода.
Тернарный оператор
Частным случаем условий является тернарный оператор, который позволяет выполнить присваивание переменной в зависимости от условия.
Условие ? Если верно : Если неверно
Пример:
int x = 100;
string msg = (x < 100) ? "Меньше ста" : "Больше или равно сотне";
Switch-case
Альтернативой триаде "if-else-else if" является блок switch-case. Но он используется только если нужно перебрать значения и совершить необходимые действия в зависимости от результата:
switch (значение)
{
case значение1:
// если значение1
break;
case значение2:
// если значение2:
break;
...
default:
// если ничего из case не совпало
break;
}
Рассмотрим такой пример простого калькулятора:
int a = int.Parse(Console.ReadLine());
int b = int.Parse(Console.ReadLine());
string input = Console.ReadLine();
switch (input)
{
case "+":
Console.WriteLine(a + b);
break;
case "-":
Console.WriteLine(a - b);
break;
case "*":
Console.WriteLine(a * b);
break;
case "/":
Console.WriteLine(a / b);
break;
default:
Console.WriteLine("Некорректная команда!");
break;
}
?В современном C# громоздкий switch-case часто заменяется другими конструкциями, но знать базовый синтаксис важно.
Switch-expression
Пример с температурой можно красиво описать через switch expression.
string tempDescription = t switch
{
< 0 => "Очень холодно",
< 10 => "Холодно",
< 20 => "Прохладно",
< 30 => "Тепло",
_ => "Жарко"
};
Console.WriteLine(tempDescription);
Оператор пропуска (_) значения равносилен default ветке классического switch.
6. Nullable аннотации и структуры
Nullable Reference аннотации
Хотя и все ссылочные типы поддерживают null значение, оно не всегда считается корректным значением. В современном C# считается хорошим тоном явно указывать, что переменная может иметь null значение, как корректное значение. Для этого нужно использовать вопросительный знак после типа.
string? input = Console.ReadLine(); // ждем текст или null
Это делается для того, чтобы другие разработчики (вы же будете работать в команде), точно могли понять, что null является допустимым значением переменной.
Nullable структуры
Другой интересной возможностью являются Nullable структуры. По умолчанию значимые типы не могут иметь значение null, но стоит указать их с вопросительным з��аком, как это возможность появится.
bool? threeStateFlag = null; // этот bool может иметь значения true\false\null
Можно использовать такой bool для хранения статуса игрока:
true - в игре
false - вышел
null - ошибка при получении статуса
Кстати, раньше использовалась следующая запись:
Nullable<bool> threeStateFlag;
Аналогичная логика может быть и для чисел. Допустим, все возможные численные значения допустимы, как тогда показать, что метод ничего не вернул? Сделать int nullable:
int? result = null;
// некая логика получения значения
7. Исключения и их обработка
Представим ситуацию. Вы делаете деление двух чисел, которые до этого запросили у пользователя.
string strA = Console.ReadLine();
string strB = Console.ReadLine();
int a = int.Parse(strA);
int b = int.Parse(strB);
Console.WriteLine(a / b);
Казалось бы, что может пойти не так? Как минимум, int.Parse может привести к исключению, если пользователь введет буквы или другие символы.
string? strA = Console.ReadLine();
string? strB = Console.ReadLine();
int a, b;
if (int.TryParse(strA, out a) && int.TryParse(strB, out b))
{
Console.WriteLine(a / b);
}
А теперь все безопасно, не так ли? Внезапно, нет! Попробуйте ввести вторым числом ноль…
> Вызвано исключение DivideByZeroException
Именно для этого и многих другие случаев существует обработка исключений.
try-catch
try
{
// блок, в котором может возникнуть ошибка
}
catch
{
// блок, который выполните только при возникновении ошибки
}
В играх и других серьезных программах, мы обычно не пытаемся исправить ошибку, а записываем сам факт ее возникновения в лог. Exception является родительским классом для всех исключений, поэтому, если указать его, мы будем отлавливать все исключения. Запустите этот код, что увидеть вывод текста (свойство Message) исключения в консоль:
try
{
int i = int.Parse(" ");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Хорошей практикой является отлов конкретных ошибок.
Вернемся к примеру с делением чисел.
try
{
Console.WriteLine(a / b);
}
catch (DivideByZeroException)
{
Console.WriteLine("Ошибка деления на 0");
}
Однако, в данном примере можно просто сделать проверку, что b не является нулем. В серьезных программах исключения возникают в более сложных ситуациях, например, мы хотим получить файл сохранений пользователя, чтобы загрузить его, но по какой-то причине не удается прочитать этот файл.
Блок finally будет выполнен в любом случае, вне зависимости от того, возникло исключение или нет.
Какие исключения вам часто придется видеть?
OverflowException - переполнение возникает если вы попытаетесь в меньший тип записать значение большего типа (например, в int значение long).
FormatException - неправильный формат данных, например, при попытке получить из текста int в int.Parse.
ArgumentNullException - в метод был передан null-аргумент, а метод ожидал данных.
NullReferenceException - обращение к данным, имеющим значение null. Чаще всего означает, что вы пропустили инициализацию объекта, но нигде не проверили этого.
StackOverflowException и OutOfMemoryException - из-за ошибки в логике память переполнилась (подробнее в разделе про циклы).
8. Работа с символами и строками
Символы
Для символов доступно множество методов проверок:
IsDigit - является ли символ цифрой
IsLetter - является ли символ буквой
IsLetterOrDigit - является ли символ цифрой или буквой
IsWhiteSpace - является ли символ пробельным (пробел, табуляция, разрыв строки)
IsCommand - является ли символ управляемым
IsLowerCase - является ли символом нижнего регистра
IsUpperCase - является ли символом верхнего регистра
Строки
Со строками у нас в распоряжении еще больше интересных методов, их стоит рассмотреть более подробно.
Проверка строки на пустоту
Бывает так, что вы ожидаете от пользователя осмысленной текста, а он просто нажмет Enter. В таком случае вы можете получить исключение при попытке работать с такой строкой. Как быть?
string input = Console.ReadLine();
if (string.IsNullOrEmpty(input))
{
Console.WriteLine("Строка пуста");
}
if (string.IsNullOrWhiteSpace(input))
{
Console.WriteLine("Строка пуста или состоит из пробелов");
}
Изменение регистра строки
string name = "Коля";
Console.WriteLine(name.ToUpper()); // КОЛЯ
Console.WriteLine(name.ToLower()); // коля
Substring
Получение подстроки из строки
string player = "Pro100 Vasya";
Console.WriteLine(player.Substring(0, 6)); // Pro100
В настоящее время удобнее использовать диапазоны из дальнейших глав.
Split
Этот метод позволяет из одной строки получить несколько, разбив ее на части. В качестве аргумента принимается символ, который служит разделителем.
Для примера разделим числа, между которыми стоит запятая:
var numParts = "10,20,30".Split(','); // [ "10", "20", "30" ]
Результатом является массив строк. Про массивы будет в следующей главе.
Join
Этот метод позволяет объединить несколько строк в одну, при этом можно использовать разделитель.
var arr = ["Скелет", "Зомби", "Призрак"];
Console.WriteLine(string.Join(arr, ',')); // Скелет,Зомби,Призрак
IndexOf
Этот метод вернет индекс вхождения аргумента в строку или -1, если ничего не нашел.
string targetAddress = "127.0.0.1:888";
if (targetAddress.IndexOf(":") >= 0)
{
Console.WriteLine("URL содержит порт");
}
LastIndexOf
То же самое, но ищет индекс последнего элемента.
StartsWith
Вернет true, если строка начинается с аргумента.
string site = "https://ya.ru";
if (site.StartsWith("https://"))
{
Console.WriteLine("Сайт с HTTPS");
}
EndsWith
Вернет true, если строка заканчивается аргументом.
string site = "https://google.com";
if (site.EndsWith(".com"))
{
Console.WriteLine("Сайт заканчивается на .com");
}
Trim
При использовании без аргументов очищает пробельные символы с начала и конца. Можно явно передать символы для удаления.
string dirtyString = " фрагмент текста ";
Console.WriteLine(dirtyString.Trim()); // фрагмент текста
Этот метод очень часто используется для фильтрации пользовательского ввода, ведь очень легко и часто происходит так, что человек нажимает лишний раз пробел или вставляет текст из другого источника.
TrimStart
Аналогично, но убирает символы только с начала.
TrimEnd
Аналогично, но убирает символы с конца.
?Задание: как можно разбить строку IP адреса на составляющие? Например, такую:
string myIp = "245.123.0.54";
9. Null-операторы
Условный null оператор
string? input = Console.ReadLine();
string? trimedInput = input.Trim();
Что здесь не так? Если строка действительно пуста, то Trim на null даст NullRerenceException. Для защиты от такого поведения мало просто пометить строку как nullable, но можно выполнить Trim только если input не null.
string? input = Console.ReadLine();
string? trimedInput = input?.Trim();
С помощью условного null-оператора (?.) методы выполнится только стоящее перед ним значение (input) не null.
Оператор null присваивания
Было бы логичным иметь не только условие, защищенное от null, но и возможность выполнять присваивание. И такая возможность появилась в C# 14. Ее синтаксис точно такой же, но раскрою эту тему в главе с классами.
10. Структуры данных
По-настоящему важная тема! Если нам нужно какое-то значение, например, параметры юнитов, мы можем создавать переменные. Идея супер, но как только нам нужно 20, 50, 100 и больше таких юнитов с параметрами, что делать? Для этого и придуманы структуры данных, в них можно хранить множество значений, оперируя при этом одним "контейнером".
Массив
Массив (array) является простой структурой данных, который описывается квадратными скобками ([]) возле имени типа. Он не может менять свой размер динамично.
int[] emptyArray = new int[10]; // пустой массив из 10 элементов
Можно сразу же заполнить массив значениями, не указывая его размер:
int[] someNums = new int[] { 0, 1, 2, 3, 4, 5 };
Вот так мы создали массив с некоторыми числами. К счастью, такой громоздкий синтаксис стал проще начиная с C# 12, поэтому можно писать так:
bool[] myFlags = [true, false, false];
int[] nums = [1, 3, 5, 12];
Узнать длину массива можно через свойство Length:
int[] arr = [1, 2, 3];
int length = arr.Length; // 3
Важно запомнить, что индексы мы всегда считаем не с 1, а с 0, поэтому доступ к первому элементу указывается через индекс 0, ко второму - через 1 и т.д.
decimal[] prices = [25m, 5m, 1000m];
decimal secondPrice = prices[1]; // 5
Массивы массивов
?Нет ничего, что запрещало бы нам сделать массивы массивов!
int[][] matrix = new int[]
{
new int[] { 1, 0, 1 },
new int[] { 0, 0, 0 },
new int[] { 1, 1, 1 },
};
Или в более новом и красивом виде:
int[][] matrix =
[
[ 1, 0, 1 ],
[ 0, 0, 0 ],
[ 1, 1, 1 ],
];
for (int y = 0; y < matrix.Length; y++)
{
for (int x = 0; x < matrix[y].Length; x++)
{
Console.Write(matrix[y][x]);
}
Console.WriteLine(); // для перевода строки
}
Многомерные массивы
Перепишем прошлый пример с использованием многомерного массива:
int[,] points = { { 1, 0, 1 }, { 0, 0, 0 }, { 1, 1, 1 } };
Класс Array
В классе Array есть множество методов для работы с массивами:
Clear - очищает массив
Fill - заполняет массив указанным значением
Exist - поиск элемента в массиве
Copy - скопировать один массив в другой
Resize - изменяет размер массива на указанный
Sort - сортировка массива
Список
Можно думать о списке как о динамичном массиве, потому что список может менять свой размер.
Создание списка и других структур данных будет отличаться от создания массива. Так мы обязаны указывать тип в треугольных скобках:
List<тип> имя = new List<тип>();
Например:
List<int> years = new List<int> { 2004, 2005, 2022 };
Или начиная с C# 12:
List<int> years = [2004, 2005, 2022];
Важные методы списка:
Add - добавить элемент в список.
Remove - удалить элемент списка.
IndexOf - узнать индекс элемента.
RemoveAt - удалить элемент по нужному индексу.
Clear - очистить.
Попробуем заполнить список строк:
List<string> names = new();
names.Add("Виктор");
names.Add("Анна");
names.Add("Дональд");
Важное замечание: у массива свойство Length всегда равно количеству элементов. Если массив не заполнен явно, то элементы будут иметь значение по умолчанию (null или default соответственно).
?У коллекций (List, Dictionary и т.д.) используется Count, потому что их размер может динамически меняться.
В большинстве случаев вы будете пользоваться именно массивами и списками, но на них все не заканчивается!
Стек
Хранит элементы в порядке LIFO (last input - first output), что можно сравнить со стопкой тарелок: она растет снизу вверх, но убирать их придется сверху вниз.
Stack<string> stackWords = new Stack<string>();
stackWords.Push("Привет");
stackWords.Push("мир!");
Console.WriteLine(stackWords.Pop());
Console.WriteLine(stackWords.Pop());
Выводит текст в обратном порядке:
мир!
Привет!
Очередь
Хранит элементы по порядку FIFO (first input - first output), или, проще говоря - в порядке очереди.
Queue<string> queueWords = new Queue<string>();
queueWords.Enqueue("Привет");
queueWords.Enqueue("очередь!");
Console.WriteLine(queueWords.Dequeue());
Console.WriteLine(queueWords.Dequeue());
Выводит текст в правильном порядке:
Привет
очередь!
?Как может пригодиться очередь? В той же Dota 2 мы запоминаем каждого, кто нанес урон герою за последние несколько секунд, чтобы потом поделить награду между этими героями.
Словарь
Хранит элементы парами ключ-значение.
Особенность такого подхода в использовании двух типов: тип ключа и тип значения. Это могут быть как одинаковые типы (string-string), так и разные (string-int и т.д.).
Самый простой пример: учет количества товаров, где ключ - название, а значением является их количество.
Dictionary<string,int> productsCount = new Dictionary<string,int>();
productsCount.Add("банан", 25);
productsCount.Add("яблоко", 50);
productsCount.Add("кокос", 10);
Console.WriteLine(productsCount["яблоко"]); // 50
А вот при попытке добавить ключ, который уже есть в словаре, будет исключение (ошибка).
Свойство Keys позволяет получить ключи словаря, а свойство Values - значения.
?Характеристики героев �� RPG или MOBA играх легко представить как словарь, где ключом является название (сила, ловкость, интеллект), а значениями их число.
?Задание: самостоятельно создайте словарь, где ключ - int, а значение - string. Добавьте несколько чисел в словарь, чтобы значениями являлось письменное название числа на русском (0 - "ноль" и т.д.). Выведите несколько значений в консоль.
11. Циклы
Проще говоря, циклы (loops) позволяют выполнять действия определенное количество раз или пока не будет выполнено какое-либо условие.
Цикл for
Самый популярный цикл - цикл с счетчиком.
for (откудаНачинаем; доСкольки; меняемСчетчик)
{
// тело цикла
}
Например, цикл от 1 до 10:
for (int i = 1; i <= 10; i++)
{
Console.WriteLine(i);
}
Заметьте, что общепринятой практикой является операция инкремента для увеличения счетчика на единицу после каждого шага.
По-настоящему сила структур данных раскрывается именно при изучении циклов. Мы можем легко вывести значения любого массива или других коллекций:
int[] myNums = [21, 45, 69, 75, 104, 2000, -1, 2, 55];
for (int i=0; i<myNums.Length; i++)
{
Console.WriteLine(myNums[i]);
}
Цикл foreach
Тоже популярный цикл по коллекциям.
foreach (чтоБерем in отЧегоБерем)
{
// тело цикла
}
Применять его можно по строкам и многим структурам данных, которые поддерживают такую возможность (массивы, списки, словари и т.д.).
foreach (char c in "Разбери меня на символы")
{
Console.WriteLine(c);
}
?Когда пользоваться for, а когда foreach? Если коллекция может меняться в процессе ее перебора, то вам нужен for, потому что foreach не позволяет менять коллекцию во время ее перебора. Также, если вам обязательно нужен индекс, а не просто перебор, вам понадобится именно for. Для всех остальных случаев используйте foreach.
Цикл while
Выполняется пока условие истинно.
int x = 10;
while (x > 0)
{
x--;
}
Console.WriteLine(x); // 0
С циклом while особенно легко ошибиться, сделав его бесконечным. Например:
int a = 100;
int b = 1;
while (b > 0)
{
a -= b;
}
На самом деле логично было сделать a > 0 в условии. Но из-за этой опечатки цикл, как не парадоксально, зациклится. Да, на самом деле он не бесконечный: в какой-то момент a дойдет до своей нижней границы, и мы получим исключение.
Цикл do while
Это цикл с постусловием и самый редко используемый цикл. Аналогичен while, но сначала выполняет действия, а лишь потом проверяет условие. Это значит, что действия будут выполнены как минимум один раз.
do
{
// тело цикла
} while (условие)
Continue и break
Иногда очень нужно пропустить текущий шаг цикла, например, мы хотим вывести все положительные числа. Нам поможет оператор continue (продолжение):
int[] someNums = [-5, 2, -1, 0, 55, 100, -44, 256, 1000];
for (int i=0; i<someNums.Length; i++)
{
if (someNums[i] < 0) continue;
Console.WriteLine(someNums[i]);
}
Оператор break (прервать) полностью прерывает выполнение цикла.
Например, мы хотим найти 0 и записать его индекс в том же массиве из примера с continue. Т.к. поиск после нахождения нуля не имеет смысла, мы просто выйдем из цикла:
int indexOfZero;
for (int i=0; i<someNums.Length; i++)
{
if (someNums[i] == 0)
{
indexOfZero = i;
break;
}
}
Поздравляю! Теперь вы знаете, как работают простейшие методы поиска!
?Задание: попробуйте догадаться, что выведет следующий код:
for (int i = 10; i > 0; i--)
{
Console.WriteLine(i);
}
12. Методы и аргументы
До этого момента мы не выходили за пределы точки входа (Main). Теперь я предлагаю вам создавать методы рядом с Main (ниже или выше - неважно).
Почему одни методы мы просто вызываем (Console.WriteLine), а вызов других (Console.ReadLine) записываем в переменные? Все просто, методы, описанные без типа (void) не могут возвращать значение, поэтому и записать результат их выполнения мы не можем. Рассмотрим примерное описание указанных выше методов:
void WriteLine(string text)
{
// тело метода
}
string ReadLine()
{
// тело метода
}
Можно увидеть, что WriteLine принимает аргументом строку (string text), но ничего не возвращает (void). В то же время ReadLine возвращает строку (string), но ничего не принимает. Именно описание метода (сигнатура) определяет то, как мы будем вызывать метод в своем коде.
Обращаясь к методам по имени, мы его вызываем. Если метод требует наличия аргументов, мы обязаны их передать при вызове.
Напишем свой метод, который будет складывать два числа:
static int Add(int a, int b)
{
return a + b;
}
Слово static всего лишь значит, что метод доступен для всего класса в целом, а не для его экземпляров (об этом позже). Метод имеет тип int, значит его результатом тоже должно быть int. Два аргумента (a, b) тоже имеют тип int, значит именно значения этих типов нужно передать. Вызываем наш метод (можно сразу передать в WriteLine):
Console.WriteLine(Add(200, 1000)); // 1200
Видим результат в консоли.
Перегрузки методов
Вы могли заметить, что мы передали в WriteLine число, а не строку. Почему это работает? Можно написать несколько вариантов одного и того же метода, но с разными типами аргументов. Это называется перегрузкой (overloading). Напишем метод, выводящий значение в консоль, чтобы было нагляднее.
static void Print(int i)
{
Console.WriteLine("Вывод целого числа" + i);
}
static void Print(string str)
{
Console.WriteLine("Вывод строки " + str);
}
И попробуем вызвать этот метод с числами и со строками:
Print("привет"); // вызов Print(string)
Print(12345); // вызов Print(int)
Print(0); // вызов Print(int)
Print("яблоко"); // вызов Print(string)
Как можно заметить, будет вызываться именно та версия метода, которая имеет подходящий тип.
Также вы можете использовать разное количество аргументов для перегрузки. На примере того же Add напишем несколько вариаций:
static int Add(int a, int b)
{
return a + b;
}
static int Add(int a, int b, int c)
{
return a + b + c;
}
static int Add(int a, int b, int c, int d)
{
return a + b + c + d;
}
Теперь подходящий метод будет выбираться в зависимости от того, сколько методов мы передали.
Add(2, 3); // 5
Add(10, 11, 65); // 86
Add(4, 100, 55, 12); // 171
Передача любого числа аргументов
Прошлый пример можно значительно упростить, если знать ключевое слово params:
static int Add(params int[] arr)
{
int value = 0;
for (int i=0; i<arr.Length; i++)
{
value += arr[i];
}
return value;
}
Главное, это указать params вместе с массивом!
Теперь можно вызывать метод с любым количеством аргументов!
Add(2); // 2
Add(3, 4); // 7
Add(33, 5, 10); // 48
Add(1, 1, 1, 1); // 4
Add(5, 6, 1, 2, 3); // 17
И так далее.
Модификаторы ref, out, in
Некоторые методы могут иметь аргументы, помеченные одним из этих модификаторов.
Модификатор ref (от слова reference) означает, что мы передаем аргумент по ссылке. Хорошей иллюстрацией будет попытка изменить значение значимого типа, переданного в метод:
void Increment(int x)
{
x++;
}
Но если вызвать метод, нас ждет сюрприз:
int x = 5;
Increment(x);
Console.WriteLine(x); // 5
Дело в том, что значимые типы копируются каждый раз. Поэтому наша переменная не могла поменяться, в методе мы увеличивали ее копию. Но стоит добавить модификатор ref:
void Increment(ref int x)
{
x++;
}
И результат будет логичным!
int x = 5;
Increment(ref x);
Console.WriteLine(x); // 6
Модификатор out (от output) означает, что аргумент может быть инициализирован заранее, ведь его значение будет возвращено из метода. В самом простом случае можно использовать out для возвращения еще одного значения, помимо основного, возвращаемого методом.
Хорошим примером являются методы TryParse типов int, long и других, о чем будет рассказано в следующей главе.
Модификатор in (от input) указывает, что аргумент передается по ссылке, но не может быть изменен.
13. Приведения типов
Неявные приведения
Значения некоторых типов можно приводить (casting) друг к другу без явного указания разработчика, чаще всего это приведения значения меньших типов к большим:
int myInt = 1000;
long myLong = myInt; // 1000
Или
float myFloat = 1.555f;
double myDouble = myFloat;
Никаких специальных усилий не потребовалось!
Явное приведение
Но в большинстве случаев нужно явно указать тип результата в скобках.
int myNum = (int)1.123; // 1
char x = (char)120; // 'x'
Важно заметить, что приведение вещественных чисел к целым не будет округлять число, а только отбросит дробную часть.
Методы Parse и TryParse
Многие примитивные типы имеют методы для приведения типов. Например, в int.Parse можно передать строку, в результате будет возвращено число.
int i = int.Parse("5000");
Если строка не содержала число, вы получите исключение.
Отличие TryParse в том он возвращает bool, а число нужно передать аргументом со словом out. И при неправильном приведении вы не получите исключение, просто сам метод вернет false. Это будет означать, что приведение не удалось и работать с числом нельзя.
string input = Console.ReadLine();
if (int.TryParse(input, out int num))
{
Console.WriteLine("было введено число " + num);
}
else
{
Console.WriteLine("введено не число");
}
Класс Convert
Класс Convert предоставляет множество методов для приведения одних типов к другим.
long largeNum = 123456789;
int num = Convert.ToInt32(largeNum);
В этом классе множество методов и перегрузок для них.Обратите внимание, что в названиях методов используются реальные названия типов (Single вместо float, Int32 вместо int и т.д.).
Метод ToString
Очень полезным является метод ToString (приведение к строке), который можно применять к значениям любых типов. Такая гибкость достигается тем, что этот метод предоставляется классом object, а значит доступен всем производным типам (вспоминаем, что он является родителем для всех типов).
int myNum = 1024;
string myNumStr = myNum.ToString();
Для некоторых типов ToString предоставляет перегрузки с формированием строки или другими важными параметрами, например:
float speed = 9.7555f;
Console.WriteLine(speed.ToString("F2")); // 9.75
14. Математические операции
Класс Math предоставляет множество методов для математических операций, выделю лишь самые часто используемые из них.
Math.Min - минимальное из двух чисел
Math.Max - максимальное из двух чисел
Math.Clamp - ограничивает переменную нижним и верхним пределами.
Эти три метода очень часто используют в разработке игр, да и в программировании в целом.
Например, мы можем двигать курсор вверх, но не хотим, чтобы он выходил за пределы размера экрана (в примере это 1920):
float x = Math.Max(1920, x - 1);
Другие методы тоже будут вам полезны:
Math.Round - округление до ближайшего целого
Math.Floor - округление до наименьшего целого
Math.Abs - абсолютное значение числа (модуль)
Math.Sqrt - квадратный корень от числа
Math.Pow - возведение в степень
int n = (int)Math.Pow(2, 10); // 1024
Кстати, если очень хочется почувствовать себя хакером, можно сделать возведение в 10 степень с помощью битового сдвига единицы влево:
int n = 1 << 10;
Возвращаясь к Math. Помимо перечисленных выше методов, вы можете найти и другие, с необходимыми тригонометрическими функциями (Sin, Cos и т.д.).
Большинство методов Math возвращают результат типа double, что не всегда удобно. К счастью, есть версия класса Math для float типа - MathF.
15. Работа с датой и временем
Мы уже затрагивали структуру DateTime в качестве примера. Она представляет конкретный момент времени.
var millenium = new DateTime(2000, 1, 1);
Время в текущий момент можно получить свойством Now:
var now = DateTime.Now;
Console.WriteLine(now);
Но вам вряд ли понравится вывод в консоль в стандартном виде. К счастью, как и вещественные числа, DateTime можно форматировать для удобного представления, например, вывести точную дату в российском формате:
Console.WriteLine(now.ToString("dd.MM.yyyy hh:mm:ss"));
DateTime имеет множество методов для сложения и вычитания отдельных промежутков времени. Помимо этого, мы можем напрямую вычесть или сложить два DateTime. Однако, неожиданным может оказаться результат, ведь он будет иметь тип TimeSpan.
Узнаем, сколько времени прошло с момента рождения и до текущего момента:
var birthDay = new DateTime(1989, 2, 3);
var now = DateTime.Now;
var delta = now - birthDay;
TimeSpan указывает не на конкретный момент времени, а скорее содержит информацию об отрезке времени.
?Задание: попробуйте вывести в консоль только часы, минуты и секунды текущей даты.
16. Работа со случайными числами
На самом деле они псевдослучайные, но это уже тонкости, я буду называть их случайными в этой главе. Для генерации случайных чисел используется класс Random.
var rand = new Random();
Метод Next позволяет получить случайное целое число в диапазоне от 0 до X-1.
int randInt = rand.Next(1000); // от 0 до 999 включительно
Если использовать перегрузку с двумя аргументами, то можно получить целое число от X до Y-1.
int randInt = rand.Next(25, 100); // от 25 до 99 включительно
В современном C# вы можете не создавать новый экземпляр Random, а сразу использовать его свойство Shared, которое предоставляет уже готовый экземпляр.
int randInt = Random.Shared.Next(10); // от 0 до 9 включительно
В играх очень часто используются случайные числа. Во многих стратегиях и RPG атака юнита в момент удара рассчитывается по механикам из настольных игр:
Атака = Базовая атака + Бросок кости(Количество костей, Количество граней кости)
Что можно представить как:
int baseAttack = 10;
int diceCount = 2;
int diceEdges = 3;
int attack = 10 + Random.Shared.Next(0, diceCount * diceEdges);
17. Индексы и диапазоны
Раньше мы могли обращаться к индексу только через int, получая элемент по этому индексу. Начиная с C# 8 у разработчиков появились индексы и диапазоны как мощный и удобный инструмент работы с коллекциями.
Индексы
Индекс (System.Index) - тип, который можно передавать для доступа к элементам.
Индекс с конца начинается с 1 и должен начинаться со специального префикса ^.
int[] nums = [1,2,3,4,5];
Console.WriteLine(nums[^1]); // 5
foreach (int n in nums[1..3])
{
Console.WriteLine(n); // 2, 3
}
Диапазоны
Диапазон (System.Range) - тип, который позволяет получить несколько элементов из коллекции.
x.. - получить все элементы, начиная с x
x..y - получить все элементы с x до y - 1.
..y - получить все элементы до y - 1.
int[] nums = [ 1, 2, 3, 4, 5 ];
var slice = nums[1..3]; // [ 2, 3 ]
18. Интерполяция строк
Часто приходится "склеивать" строки, состоящие из разных частей:
int a = 10;
int b = 25;
string str = a + " " + b + " = " + (a b);
Очень удобным средством является интерполяция строк. Для ее использования нужно указать знак доллара ($) перед кавычками. После этого можно использовать любые значения или переменные в фигурных кавычках - они автоматически будут преобразованы в строки.
Перепишем пример и с использованием интерполяции:
int a = 10;
int b = 25;
string str = $"{a} {b} = {a b}";
Если же каким-то образом нужно вывести в строке фигурные скобки, то их нужно делать парными с каждой стороны, тем самым экранируя их.
string str = $"{{внутри скобок}}"; // {внутри скобок}
19. StringBuilder
Я уже упоминал, что строки всегда создаются, но не меняются. Следовательно, работа с ними в некоторых случаях может оказаться требовательной к ресурсам. Лучший способ работы с большим количеством строк (например, в цикле) - StringBuilder.
var buffer = new StringBuilder();
for (int i=0; i<100; i++)
{
buffer.Append(i + ",");
}
Console.WriteLine(buffer.ToString());
Методы Append и AppendLine используются для добавления значений в буфер, а ToString для формирования результирующей строки.
Иногда мы можем знать, сколько места нам потребуется в буфере. В таком случае можно сразу указать это число, чем мы сэкономим время компьютеру:
var fixedBuffer = new StringBuilder(200); // точно знаю, что нужно 200 символов
?Задание: сохранить в StringBuilder 5 вводов пользователя, а потом вывести их в консоль с нумерацией от 1 до 5 (например: 1 Вася 2 Петя и т.д.)
20. Типичные ошибки новичков
Мы разобрали большое количество информации и теперь закрепим ее на примерах и антипримерах.
1. Попытка парсинга вещественных чисел с точкой
Хотя мы и пишем в коде вещественные числа, используя точку для разделения частей числа, C# учитывает настройки культур при парсинге.
float f = float.Parse("1.25"); // исключение FormatException
Решением может быть или использование запятой в таких сценариях, или более правильное - явное указание формата в Parse.
float f = float.Parse("1.25", CultureInfo.InvariantCulture);
2. Преждевременные оптимизации вредны
Новички часто склонны пытаться оптимизировать свой код раньше времени. Часто это выливается в дополнительные проблемы и, как ни парадоксально, в проблемы с производительностью.
Например, новичок хочет показать, что аргументы методы Add не меняются, поэтому добавляет модификатор in:
int Add(in int x, in int y)
{
return x + y;
}
В итоге получается передача аргумента по ссылке, что ведет к лишним накладным расходам. Но аргумент значимого типа нельзя было изменить и без передачи по ссылке!
Похожая история и с попытками экономить байты на числах. Запомните: вы смело можете использовать int в 99% задач. Иначе вас ждут бесконечные приведения между типами. Даже длина строк и коллекций это всегда int, не uint, хотя длина и не может быть отрицательной, но это сделано в целях совместимости, так исторически сложилось.
3. Не стоит доверять точности чисел с плавающей запятой
float attackMultiplier = 1.555f;
float criticalAttack = attackMultiplier * unitAttack;
if (criticalAttack == 1.555f)
{
Console.WriteLine("Атака не была усилена");
}
Вещественные числа часто могут выглядеть не совсем так, как мы ожидаем. Лучше просто избегать таких сравнений, а использовать специальные методы сравнения или операторы <= и >=, вместо строгого равенства, это убережет вас от ошибок потери точности.
4. Забывают о неизменяемости строк и некоторых структур
Мы каждый раз формируем новую строку, поэтому нет смысла просто менять старую:
void AppendEnd(string source)
{
source += "end";
}
Аналогично и со структурами:
var now = DateTime.Now;
now.AddDays(2);
AddDays возвращает новый экземпляр DateTime, а не меняет старый.
Благодарю хабровчан @axelthepop, @ProgerMan и @a-tk за замечания и найденные опечатки.
Комментарии (26)

doo000
07.01.2026 12:36Шо, опять?!!!
P.S. Гайды вчерашний день. Хорошие, плохие ли, но вчерашний. Кому сейчас надо изучать основы шарпа, если клавдия набросает каркас приложения, да еще с функционалом - только попроси. А до действительно непростых вещей автору еще долго добираться.

Jhayphal
07.01.2026 12:36Возможно, человек и не думал о C#, встретил эту статью, посмотрел и подумал: какой классный язык, надо бы его заценить.
В этом плане материал полезен :)

doo000
07.01.2026 12:36Если человек не думал о шарпе, он ему и не нужен. С этими языками почему-то всегда телега поперед лошади. "А я слышал го зашибись язык, давайте на нем проект запилим!"(ц).
Язык - средство/инструмент, проект - цель, и никак иначе. Ну или "кровавый энтерпраиз"(ц) и неподьемное наследие легаси. А читать книжки по шарпу без реального проекта - время на ветер.

sic
07.01.2026 12:36Да, мотивации в статье, кажется, не достаточно. Было бы прикольно начать с каких-нибудь примеров со знаковыми фишками языка, например LINQ. "Хотите так же? Давайте разбираться..."

Ydav359 Автор
07.01.2026 12:36Учту. Я решил приводить примеры из моего опыта в модмейкинге и наблюдениями из игр. На таких примерах информация воспринимается интереснее. Во второй части как раз есть классы с примерами в духе WarCraft 3 и т.д.

Kenya-West
07.01.2026 12:36какой классный язык, надо бы его заценить
Вы правы, это я :)
Давно уже хочу начать вместо JS использовать C#. Рантаймом будет, конечно, .NET. Вроде бы у JS и C# общих рантаймов нет, а жаль... вон, в Node.js ввозят поддержку TS, можно было бы и C# через прослойку какую-нибудь...
А ещё нейронки, когда хочешь понять основы - зло. Поддерживать своё костылеводство тоже будет нейросеть? Да как бы не так!

MaNaXname
07.01.2026 12:36за счет гибкости (ну или отсутствия типов) javascript легче навернуть систему типов TS а не C# потому что из-за отсутствия discriminated unions многие фишки TS не доступны в C#

a-tk
07.01.2026 12:36Например, новичок хочет показать, что аргументы методы Add не меняются, поэтому добавляет модификатор in:
int Add(in int x, in int y){ return x + y;}В итоге получается упаковка (boxing) типа int в object, что ведет к лишним накладным расходам.
Шта? Значение размещается в стеке, и затем передаётся ссылка на него - это да. Но, блин, БОКСИНГА здесь нет!

MaNaXname
07.01.2026 12:36о а это кстати тренд ллмок врать про боксинг при возврате значения из функции. это уже 3я статья. в первой было такое сказано про go. а во второй - про rust & go.

ProgerMan
07.01.2026 12:36Зачем начинающему показывать
Main(), если уже можно не показывать? Чем меньше нагружать грудой слов со старта, тем легче пойдёт. А проMain()и чуть позже можно разжевать.Это же сколько незнакомых слов со старта:
using,System,class,Program,static,void,Main- до которых ещё долго и сразу все не объяснить, не напугав.
Ydav359 Автор
07.01.2026 12:36Думаю, здесь нет идеального решения, я постарался минимально объяснить о стартовом проекте, сознательно убрал неймспейсы, аргументы Main. Открыл Шилдта, он тоже дает много вводных, так что получаются лишь разные вариации одного и того же, как по мне

ProgerMan
07.01.2026 12:36Однажды меня попросили научить программированию и я решил, что это отличная идея. Ещё до самого языка пришлось объяснять, что такое консольные приложения и зачем они вообще нужны. Так что программирование начинается не с кода.
А за актуальным гайдом по C# уже много лет всех отправляю сюда от ребят из "Контура".

yesenin_toxa
07.01.2026 12:36Всю дорогу в .NET не хватало от мира Джавы (или Питона, или Раста) возможности сделать один файл и запустить его. Теперь можно с dotnet run foo.cs. И я думаю для современного пути C# с file-based app будет проще учить именно язык, а не CLR. Так что как раз сейчас достаточно VS Code или nvim.

WhiteBehemoth
07.01.2026 12:36В мотивационную часть я бы добавил, что в перспективе, учитывая текущий тренд, C# выйдет за пределы классического компилируемого ЯП и зайдёт в нишу скриптов. Что сильно расширяет проф. горизонт.
Уже сейчас можно просто "выполнить скрипт"dotnet run script.cs- не нужен ни проект, ни среда разработки, только SDK.А то и просто выполнять команды прямо в консоле (пока правда через установку, например, https://github.com/waf/CSharpRepl ). Классная, кстати, вещь и легко добавляется в терминал для удобного запуска.
stanukih
Статья это переделанное выступление к первокурсникам?
Ydav359 Автор
Не понял, о чем вы. В любом случае, не вижу ничего плохого в ЦА в виде студентов
Dhwtj
Геймдев в России мертв по объективным причинам: закрыты внешние рынки продаж, внутренний рынок мал
msdos9
А вот когда Пажитнов придумывал Тетрис, он про внешние и внутренние рынки не думал...