Fundamentals

Структура програми на C#

Розуміння структури програм на C# є фундаментальним для ефективної розробки. Ця тема охоплює еволюцію від класичного підходу до сучасних [top-level statements](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/program-structure/top-level-statements), а також механізми документування коду.

Проблема: Церемоніальний код

Традиційно, навіть найпростіша програма на C# вимагала написання значної кількості "церемоніального" коду:

using System;

namespace Application
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

Для виведення одного рядка потрібно було визначити namespace, клас та метод Main. Це створювало бар'єр для початківців та ускладнювало написання простих скриптів.

Історична довідка: До C# 9.0 (2020), кожна програма вимагала явного визначення класу та методу Main. Це було успадковано від Java та C++, де структура програми є більш ригідною.

Анатомія класичної програми

Компоненти структури

Розглянемо традиційну структуру програми та роль кожного компонента:

Loading diagram...
graph TD
    A[Program.cs] --> B[using Directives]
    A --> C[Namespace Declaration]
    C --> D[Class Definition]
    D --> E[Main Method]
    E --> F[Application Code]

    B --> B1["Імпорт System"]
    B --> B2["Імпорт інших NS"]

    C --> C1["Логічна група типів"]

    D --> D1["Контейнер для Main"]

    E --> E1["Entry Point"]
    E --> E2["string[] args"]

    style E fill:#f59e0b,stroke:#b45309,color:#ffffff
    style A fill:#64748b,stroke:#334155,color:#ffffff
    style F fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style B fill:#64748b,stroke:#334155,color:#ffffff
    style C fill:#64748b,stroke:#334155,color:#ffffff
    style D fill:#64748b,stroke:#334155,color:#ffffff
    style B1 fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style B2 fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style C1 fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style D1 fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style E1 fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style E2 fill:#3b82f6,stroke:#1d4ed8,color:#ffffff

1. Using Directives (Директиви імпорту)

using System;
using System.Collections.Generic;
using System.Linq;

Призначення: Імпортують namespaces, щоб уникнути повної кваліфікації типів.

namespace MyApp
{
    class Program
    {
        static void Main()
        {
            // Повна кваліфікація типу
            System.Console.WriteLine("Hello");
            System.Collections.Generic.List<int> numbers = new();
        }
    }
}
Best Practice: Розміщуйте using директиви поза namespace (C# 10+) для покращення читабельності та уникнення проблем з вкладеними namespace.

2. Namespace (Простір імен)

namespace MyCompany.MyProduct.Utilities
{
    // Класи, інтерфейси, структури
}

Мета: Організація типів у логічні групи та уникнення конфліктів імен.

Два стилі оголошення:

namespace MyCompany.MyProduct
{
    class Calculator
    {
        // Вміст класу
    }

    class Logger
    {
        // Вміст класу
    }
}
Рекомендація: Використовуйте file-scoped namespaces (C# 10+) для зменшення рівня вкладеності та підвищення читабельності коду. Це усуває один рівень відступів у всьому файлі.

3. Class Definition (Визначення класу)

class Program
{
    // Члени класу
}

У традиційній структурі, клас Program служить контейнером для точки входу.

4. Main Method (Точка входу)

Метод Main — це спеціальний метод, який виконує CLR (Common Language Runtime) при запуску програми.

Сигнатури методу Main:

СигнатураОписВикористання
static void Main()Без параметрів, без поверненняПрості програми без аргументів
static void Main(string[] args)З аргументами командного рядкаОбробка параметрів запуску
static int Main()Повертає код завершенняСкрипти, CLI інструменти
static int Main(string[] args)Повний контрольПрофесійні CLI застосунки
static async Task Main()Асинхронна точка входу (C# 7.1+)Async/await у Main
static async Task<int> Main(string[] args)Асинхронна з поверненнямAsync CLI з exit codes

Приклад з аргументами:

using System;

namespace FileProcessor
{
    class Program
    {
        static int Main(string[] args)
        {
            if (args.Length == 0)
            {
                Console.WriteLine("Usage: FileProcessor <filename>");
                return 1; // Exit code для помилки
            }

            string filename = args[0];
            Console.WriteLine($"Processing file: {filename}");

            // Логіка обробки файлу

            return 0; // Успішне виконання
        }
    }
}

Виклик з командного рядка:

dotnet run -- myfile.txt
CLR та Entry Point: Під час компіляції, компілятор C# шукає метод з іменем Main у всіх класах. Якщо знайдено більше одного Main, необхідно вказати головний клас через налаштування проєкту (<StartupObject>). CLR викликає цей метод для початку виконання програми.

Базова структура класичного консольного проєкту:

Революція: Top-Level Statements

З C# 9.0 (.NET 5+) з'явилася можливість писати код безпосередньо у файлі, без явного визначення namespace, класу та методу Main.

Мінімальна програма

Раніше (C# 8 та нижче):

using System;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

Тепер (C# 9+):

Console.WriteLine("Hello World!"); 

Всього один рядок! Компілятор автоматично генерує всю необхідну структуру.

Базова структура проєкту з Top-Level Statements:

Loading diagram...
flowchart LR
    A["Program.cs<br/>(top-level)"] --> B["Compiler"]
    B --> C["Generated Code"]

    C --> C1["namespace (implicit)"]
    C --> C2["class Program"]
    C --> C3["static void Main"]
    C --> C4["Your Code"]

    style A fill:#64748b,stroke:#334155,color:#ffffff
    style B fill:#f59e0b,stroke:#b45309,color:#ffffff
    style C fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style C1 fill:#64748b,stroke:#334155,color:#ffffff
    style C2 fill:#64748b,stroke:#334155,color:#ffffff
    style C3 fill:#64748b,stroke:#334155,color:#ffffff
    style C4 fill:#3b82f6,stroke:#1d4ed8,color:#ffffff

Що генерує компілятор?

Коли ви пишете:

using System;

Console.WriteLine("Hello World!");

Компілятор генерує еквівалент:

using System;

internal class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
    }
}
Важливо: Top-level statements можуть бути тільки в одному файлі проєкту. Якщо спробувати використати їх у кількох файлах, компілятор видасть помилку.

Доступ до аргументів командного рядка

Магічна змінна args доступна автоматично:

// Program.cs
if (args.Length > 0)
{
    Console.WriteLine($"Hello, {args[0]}!");
}
else
{
    Console.WriteLine("Hello, World!");
}
dotnet run -- Alice
# Вивід: Hello, Alice!

Повернення коду завершення

if (args.Length == 0)
{
    Console.WriteLine("Error: No arguments provided");
    return 1; // Exit code
}

Console.WriteLine("Success");
return 0;

Асинхронний код у top-level statements

using System;
using System.Net.Http;
using System.Threading.Tasks;

// Await працює "із коробки"!
HttpClient client = new();
string content = await client.GetStringAsync("https://api.github.com");
Console.WriteLine($"Response length: {content.Length}");

Компілятор автоматично генерує:

static async Task Main(string[] args)
{
    // Ваш код
}

Комбінація з класами та методами

Top-level statements мають бути на початку файлу, а потім можна визначати інші типи:

using System;

// Top-level statements
Console.WriteLine("Starting application...");
var calculator = new Calculator();
int result = calculator.Add(5, 3);
Console.WriteLine($"Result: {result}");

// Класи та інші типи після top-level коду
class Calculator
{
    public int Add(int a, int b) => a + b;

    public int Subtract(int a, int b) => a - b;
}
Область видимості: Класи, визначені після top-level statements, знаходяться в тому ж (неявному) namespace і доступні для коду top-level statements.

Порівняння підходів

АспектКласична структураTop-level statements
Кількість рядків9+ для "Hello World"1 рядок
Точка входуЯвний Main методНеявний, згенерований
args доступПараметр MainГлобальна змінна args
Async/awaitasync Task MainПідтримується автоматично
Кількість файлівНеобмеженаТільки один файл з top-level
Підходить дляВеликі проєктиСкрипти, прототипи, навчання
Коли використовувати що?
  • Top-level statements: Прототипи, скрипти, навчальні приклади, маленькі утиліти.
  • Класична структура: Великі проєкти з множинними класами, бібліотеки, корпоративні застосунки.

Implicit Usings (Неявні імпорти)

Починаючи з C# 10 та .NET 6, SDK автоматично додає типові using директиви залежно від типу проєкту.

Увімкнення в проєкті

У файлі .csproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

Які namespaces додаються автоматично?

Для консольних застосунків (SDK: Microsoft.NET.Sdk):

// Ці using додаються автоматично:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

Результат: Ви можете використовувати Console, List<T>, File, LINQ методи без явних using директив!

// Файл Program.cs (C# 10+, ImplicitUsings enabled)

// Всі типи доступні без using!
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var evenNumbers = numbers.Where(n => n % 2 == 0);

foreach (var num in evenNumbers)
{
    Console.WriteLine(num);
}

Кастомізація глобальних using

Ви можете додати власні global using директиви, які будуть доступні у всіх файлах:

GlobalUsings.cs
global using System.Text.Json;
global using MyCompany.SharedLibrary;
global using static System.Math;

Тепер у будь-якому файлі проєкту:

// JsonSerializer, PI, Sqrt доступні глобально!
double radius = 5;
double area = PI * Pow(radius, 2);

var json = JsonSerializer.Serialize(new { Radius = radius, Area = area });
Console.WriteLine(json);
Best Practice: Створіть окремий файл GlobalUsings.cs для централізованого управління глобальними імпортами. Це покращує видимість залежностей проєкту.

Структура проєкту з Global Usings:

Коментарі та XML-документація

Документування коду є критично важливим для підтримуваності та співпраці в команді.

Види коментарів

// Це однорядковий коментар
int age = 25; // Вік користувача

XML-документація: Професійний стандарт

XML-коментарі використовуються інструментами для генерації документації (IntelliSense, DocFX, Sandcastle).

Основні теги

ТегПризначенняПриклад
<summary>Короткий опис типу/члена<summary>Основний клас для розрахунків</summary>
<remarks>Детальний опис<remarks>Цей клас використовує алгоритм...</remarks>
<param>Опис параметра методу<param name="x">Координата X</param>
<returns>Опис значення, що повертається<returns>Площа кола</returns>
<exception>Опис винятків<exception cref="ArgumentNullException">Якщо параметр null</exception>
<example>Приклад використання<example><code>var calc = new Calculator();</code></example>
<see>Посилання на інший тип/член<see cref="Calculator.Add"/>
<seealso>Зв'язані елементи<seealso cref="ICalculator"/>
<paramref>Посилання на параметр у текстіЯкщо <paramref name="x"/> менше нуля...
<typeparam>Опис generic параметра<typeparam name="T">Тип елементу</typeparam>

Комплексний приклад

namespace MathLibrary;

/// <summary>
/// Надає математичні операції для геометричних розрахунків.
/// </summary>
/// <remarks>
/// <para>
/// Цей клас оптимізований для роботи з координатами в 2D-просторі.
/// Всі методи є thread-safe.
/// </para>
/// <para>
/// Для 3D розрахунків використовуйте <see cref="Geometry3D"/>.
/// </para>
/// </remarks>
public class Calculator
{
    /// <summary>
    /// Обчислює відстань між двома точками на площині.
    /// </summary>
    /// <param name="x1">X-координата першої точки.</param>
    /// <param name="y1">Y-координата першої точки.</param>
    /// <param name="x2">X-координата другої точки.</param>
    /// <param name="y2">Y-координата другої точки.</param>
    /// <returns>
    /// Евклідова відстань між точками (<paramref name="x1"/>, <paramref name="y1"/>)
    /// та (<paramref name="x2"/>, <paramref name="y2"/>).
    /// </returns>
    /// <exception cref="ArgumentException">
    /// Викидається, якщо координати містять <see cref="double.NaN"/> або <see cref="double.PositiveInfinity"/>.
    /// </exception>
    /// <example>
    /// <code>
    /// var calculator = new Calculator();
    /// double distance = calculator.Distance(0, 0, 3, 4);
    /// Console.WriteLine(distance); // Вивід: 5
    /// </code>
    /// </example>
    public double Distance(double x1, double y1, double x2, double y2)
    {
        if (double.IsNaN(x1) || double.IsNaN(y1) || double.IsNaN(x2) || double.IsNaN(y2))
        {
            throw new ArgumentException("Координати не можуть бути NaN.");
        }

        double dx = x2 - x1;
        double dy = y2 - y1;
        return Math.Sqrt(dx * dx + dy * dy);
    }

    /// <summary>
    /// Обчислює площу кола за його радіусом.
    /// </summary>
    /// <param name="radius">Радіус кола. Має бути більше нуля.</param>
    /// <returns>Площа кола.</returns>
    /// <exception cref="ArgumentOutOfRangeException">
    /// Викидається, якщо <paramref name="radius"/> менше або дорівнює нулю.
    /// </exception>
    public double CircleArea(double radius)
    {
        if (radius <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(radius), "Радіус має бути додатнім.");
        }

        return Math.PI * radius * radius;
    }
}

Генерація XML-файлу документації

Увімкніть у .csproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
  </PropertyGroup>
</Project>

Після компіляції буде створено XML-файл з усією документацією, який можна використати для генерації веб-сайту з документацією (наприклад, через DocFX).

IntelliSense: XML-коментарі автоматично відображаються в підказках IntelliSense в IDE (Visual Studio, Rider, VS Code). Це значно полегшує розуміння API вашого коду іншими розробниками та вами самими через певний час.

Винесення документації у зовнішні файли

Для великих проєктів можна використовувати тег <include>:

Файл: MathDocs.xml

<?xml version="1.0"?>
<doc>
  <members>
    <member name="M:Calculator.Add">
      <summary>
      Додає два числа разом.
      </summary>
      <param name="a">Перше число.</param>
      <param name="b">Друге число.</param>
      <returns>Сума a та b.</returns>
    </member>
  </members>
</doc>

Файл: Calculator.cs

public class Calculator
{
    /// <include file='MathDocs.xml' path='doc/members/member[@name="M:Calculator.Add"]/*' />
    public int Add(int a, int b)
    {
        return a + b;
    }
}
Переваги зовнішніх файлів: Розділення документації та коду, можливість локалізації, менша засміченість вихідного коду. Однак, це ускладнює підтримку синхронізації між кодом і документацією.

Візуальне порівняння еволюції

Loading diagram...
timeline
    title Еволюція структури програм C#
    section C# 1.0-8.0
        2002-2019 : Класична структура
                  : namespace + class + Main
                  : Церемоніальний код
    section C# 9.0
        2020 : Top-level statements
             : Мінімалістичний синтаксис
             : Один файл з entry point
    section C# 10
        2021 : Implicit Usings
             : Global using директиви
             : File-scoped namespaces
    section C# 11-13
        2022-2024 : Покращення
                  : Raw string literals
                  : Primary constructors
                  : Collection expressions

Практичні рекомендації

Крок 1: Оберіть правильний підхід

Визначте, чи потрібна вам класична структура чи top-level statements:

  • Маленький проєкт/скрипт: Top-level statements
  • Бібліотека/великий проєкт: Класична структура

Крок 2: Налаштуйте проєкт

Увімкніть сучасні фічі у .csproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <LangVersion>latest</LangVersion>
  </PropertyGroup>
</Project>

Типова структура сучасного консольного проєкту C#:

Крок 3: Організуйте using директиви

Створіть GlobalUsings.cs для спільних імпортів:

global using System.Text.Json;
global using Microsoft.Extensions.Logging;

Крок 4: Документуйте код

Додавайте XML-коментарі до публічних API:

/// <summary>
/// Ваш опис
/// </summary>
public void MyMethod() { }

::

Практичні завдання

Рівень 1: Основи

Завдання 1.1: Від класичної структури до top-level

Створіть дві версії програми "Hello World":

  • Перша версія: з повною класичною структурою (namespace, class, Main)
  • Друга версія: з top-level statements

Порівняйте кількість рядків коду.

// Очікуваний вивід для обох версій:
// Hello, World!
// Current time: [поточний час]

Завдання 1.2: Робота з аргументами командного рядка

Напишіть програму з top-level statements, яка:

  • Приймає ім'я користувача як аргумент командного рядка
  • Якщо аргумент не надано, запитує ім'я через Console.ReadLine()
  • Виводить персоналізоване вітання
dotnet run -- Олена
# Вивід: Привіт, Олена! Ласкаво просимо.

dotnet run
# Вивід: Введіть ваше ім'я: [користувач вводить]
# Вивід: Привіт, [ім'я]! Ласкаво просимо.

Завдання 1.3: File-scoped namespaces

Перепишіть наступний код, використовуючи file-scoped namespace (C# 10+):

namespace MyCompany.Utilities
{
    public class StringHelper
    {
        public string Reverse(string input)
        {
            char[] chars = input.ToCharArray();
            Array.Reverse(chars);
            return new string(chars);
        }
    }
}

Рівень 2: Поглиблене розуміння

Завдання 2.1: Асинхронний top-level код

Створіть програму з top-level statements, яка:

  • Використовує HttpClient для завантаження JSON даних з API
  • Використовує await для асинхронного запиту
  • Парсить JSON та виводить результат
// Використайте публічний API, наприклад:
// https://api.github.com/users/[username]
// Виведіть ім'я користувача та кількість публічних репозиторіїв

Завдання 2.2: Global Usings

Створіть консольний проєкт з наступною структурою:

  • Налаштуйте ImplicitUsings в .csproj
  • Створіть GlobalUsings.cs з додатковими імпортами:
    • System.Text.Json
    • System.Text.RegularExpressions
  • Напишіть програму, що використовує ці namespace без явних using директив

Завдання 2.3: XML документація для калькулятора

Створіть клас Calculator з методами:

  • Add(int a, int b) — додавання
  • Subtract(int a, int b) — віднімання
  • Multiply(int a, int b) — множення
  • Divide(double a, double b) — ділення

Додайте повну XML-документацію для кожного методу, включаючи:

  • <summary> — опис методу
  • <param> — опис кожного параметра
  • <returns> — опис значення, що повертається
  • <exception> — опис винятків (для Divide приділенні на нуль)
  • <example> — приклад використання

Рівень 3: Практичне застосування

Завдання 3.1: CLI інструмент для обробки файлів

Створіть консольний застосунок для обробки текстових файлів:

Вимоги:

  • Використайте top-level statements
  • Прийміть шлях до файлу та команду як аргументи CLI
  • Команди: count (кількість слів), upper (у верхній регістр), lines (кількість рядків)
  • Поверніть код завершення: 0 для успіху, 1 для помилки
  • Додайте обробку помилок (файл не знайдено, невідома команда)
dotnet run -- myfile.txt count
# Вивід: Words: 150

dotnet run -- myfile.txt upper
# Вивід: [вміст файлу у верхньому регістрі]

dotnet run -- nonexistent.txt count
# Вивід: Error: File not found
# Exit code: 1

Завдання 3.2: Модульна програма з top-level statements

Створіть програму, яка демонструє комбінацію top-level statements з класами:

Структура:

  • Top-level код у Program.cs — точка входу та основна логіка
  • Класи після top-level коду:
    • UserManager — управління користувачами (список, додавання, пошук)
    • ConsoleUI — методи для взаємодії з консоллю

Функціонал:

  • Меню з опціями: додати користувача, показати всіх, знайти за іменем, вийти
  • Зберігання користувачів у List<User> (клас User з полями Name, Email, Age)
  • Валідація введення
// Приклад взаємодії:
// 1. Add user
// 2. Show all users
// 3. Find by name
// 4. Exit
// Choose option: 1
// Enter name: Іван
// Enter email: ivan@example.com
// Enter age: 25
// User added successfully!

Завдання 3.3: Генерація документації

Створіть невелику бібліотеку класів (Class Library project):

Вимоги:

  • Створіть клас MathOperations з методами для геометричних обчислень:
    • CircleArea(double radius) — площа кола
    • RectanglePerimeter(double width, double height) — периметр прямокутника
    • TriangleArea(double baseLength, double height) — площа трикутника
  • Додайте повну XML-документацію з <example> секціями
  • Налаштуйте проєкт для генерації XML-файлу документації
  • Створіть консольний проєкт, який посилається на бібліотеку
  • Переконайтеся, що IntelliSense показує вашу документацію

Налаштування .csproj бібліотеки:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
  </PropertyGroup>
</Project>
Поради для виконання завдань:
  • Експериментуйте з різними підходами (класична структура vs top-level)
  • Використовуйте IntelliSense для перегляду згенерованої документації
  • Тестуйте CLI програми з різними вхідними даними та крайніми випадками
  • Звертайте увагу на коди завершення для CLI інструментів
  • Створюйте окремі файли для різних класів у складніших проєктах

Резюме

Структура програми на C# пройшла значну еволюцію від ригідної, церемоніальної форми до гнучкого, мінімалістичного підходу. Розуміння обох підходів є критично важливим:

  • Класична структура (namespaceclassMain) забезпечує явну організацію та підходить для великих проєктів.
  • Top-level statements (C# 9+) усувають шаблонний код для простих сценаріїв та навчання.
  • Implicit usings та global using скорочують повторювані імпорти.
  • XML-документація є стандартом індустрії для професійного документування коду.