У JetBrains есть фреймворк JetBrains.Annotations для .NET, который предоставляет набор полезных атрибутов. Они выступают дополнительными метаданными как для самих разработчиков, так и для статического анализатора JB, который включён в их IDE и ReSharper.

JetBrains.Annotations доступен в nuget, но может ограниченно работать вне продуктов JetBrains. Тем не менее в System.Diagnostics.CodeAnalysis тоже есть набор стандартных полезных и похожих атрибутов.

В первую очередь, атрибуты позволяют лучше понимать как намерения автора, так логику и семантику кода, а не только его синтаксис. Это делает листинг проще (если не переборщить с обилием атрибутов) и позволяет анализатору максимально своевременно предупредить разработчика или дать ему подсказку.

В текущих реалиях у этих атрибутов появилось ещё одно полезное свойство – они учитываются нейросетью и помогают ей быстрее ориентироваться в проекте. И, как мне показалось, автокомплит тоже становится более точным (про инструменты, доступные в Rider, делал отдельную, ещё не сильно устаревшую, подборку).

Пример использования атрибутов в поиске через AI
Пример использования атрибутов в поиске через AI

Важно отметить, что атрибуты — это лишь мета-информация для анализатора, которая не влияет на runtime.

Например, использование атрибута [NotNull] для аргумента метода не добавляет автоматической проверки. В метод всё ещё можно будет передать null. И если в методе этот кейс не будет обработан, то случится NullReferenceException. Атрибут лишь подскажет IDE, что в этот метод не нужно позволять передавать null на этапе разработки.

Пример подсказок от IDE
Пример подсказок от IDE

Также важно отметить, что этот фреймворк развивается давно, а языки программирования не стоят на месте. Поэтому какие-то атрибуты для определённых версий языка могут оказаться избыточными. Так, например, появление nullable reference type в C# 8 позволяет выразить намерения по null-спецификациям средствами самого языка.

Про этот кейс, атрибуты от JB и MS, операторы ? и !, когда что и для чего использовать, можно подробнее почитать в этом лонгриде с Хабра.

Ещё один нюанс связан с тем, что для Unity-проектов атрибуты Jetbrains поддерживаются по умолчанию, но не в полном объёме. Так, например, не удастся применить [NonNegativeValue] или [ValueRange]. В теории, можно выкачать более актуальный Jetbrains.Annotations из nuget и добавить в проект. Но лично у меня не было необходимости этим заниматься.


Примеры атрибутов

Всем, кто работает с инструментами JB, рекомендую ознакомиться с полным списком атрибутов. А здесь оставлю наиболее часто встречаемые и работающие в Unity.

⚠️ Дисклеймер ⚠️

Далее будет сгенерированный код.
Особо чувствительным лучше остановиться здесь.
Код проверен, исправлен, дополнен комментариями.

// -------------------------------------------------------------------
// 1. Анализ на Null
// -------------------------------------------------------------------

// Возвращаемое значение может быть null.
[CanBeNull]
public User FindUser(int id)
{
    return _users.TryGetValue(id, out var user) ? user : null;
}

// Параметр не может быть null.
public void LogMessage([NotNull] string message)
{
    // Полезно для FailFast в runtime.
    if (message == null) throw new ArgumentNullException(nameof(message));
    Console.WriteLine(message);
}

// Коллекция не может быть null, но могут быть null её элементы.
[NotNull, ItemCanBeNull]
public List<string> GetUserNamesWithNulls()
{
    return new List<string> { "Alice", null, "Bob" };
}

// Коллекция и её элементы не могут быть null.
public void ProcessUserNames([NotNull, ItemNotNull] IEnumerable<string> names)
{
    foreach (var name in names)
        Console.WriteLine(name.ToUpper());
}

// -------------------------------------------------------------------
// 2. Контракты Параметров
// -------------------------------------------------------------------

// Если 'obj' равен null, метод останавливает выполнение.
[ContractAnnotation("obj:null => halt")]
public void GuardNotNull(object obj)
{
    if (obj == null)
        throw new ArgumentNullException(nameof(obj));
}

// Если 's' равен null, метод вернет true.
[ContractAnnotation("s:null => true")]
public bool IsNullOrEmpty(string s)
{
    return s == null || s.Length == 0;
}

// Если 'input' не null, результат тоже не null.
[ContractAnnotation("input:notnull => notnull")]
public string Decorate(string input)
{
    return input == null ? null : $"--{input}--";
}


// IDE предлагает значения из списка констант.
public void FindControl([ValueProvider("UiConstants")] string id)
{
    // ...
}

public static class UiConstants
{
    public const string PanelId = "mainPanel";
    public const string ButtonId = "okButton";
}

// -------------------------------------------------------------------
// 3. Поведение Методов
// -------------------------------------------------------------------

// Метод не имеет побочных эффектов.
// Его вызов без использования результата бессмыслен.
[Pure]
public int CalculateSum(int a, int b)
{
    return a + b;
}

// Метод может иметь побочные эффекты.
// Но его вызов без использования результата бессмыслен.
[MustUseReturnValue]
public string GetAndRemoveFirst(Queue<string> queue)
{
    return queue.Dequeue(); // побочный эффект: меняет очередь
}

// Помечают метод как Assert-метод для параметров.
[AssertionMethod]
public void MyAssert(
    [AssertionCondition(AssertionConditionType.IS_TRUE)] bool condition)
{
    if (!condition)
        throw new InvalidOperationException("Assertion failed");
}

// Метод безусловно завершает программу.
// Устаревший. Аналог: [ContractAnnotation("=> halt")]
[TerminatesProgram]
public void FatalError(string msg)
{
    Console.Error.WriteLine(msg);
    Environment.Exit(1);
}


// Вызывающий код обязан вызвать Dispose.
[MustDisposeResource]
public System.IO.FileStream OpenFile(string path)
{
    return new System.IO.FileStream(path, System.IO.FileMode.Open);
}


// -------------------------------------------------------------------
// 4. Работа со Строками
// -------------------------------------------------------------------

// 'format' - это строка формата.
[StringFormatMethod("format")]
public void Log(string format, params object[] args)
{
    Console.WriteLine(string.Format(format, args));
}

// Строка является паттерном регулярного выражения.
public void FindMatches([RegexPattern] string regexPattern, string text)
{
    var matches = Regex.Matches(text, regexPattern);
    Console.WriteLine($"Found {matches.Count}");
}

// Строка является ссылкой на путь (файл/папка).
// IDE включит проверку пути и автодополнение.
public void LoadConfig([PathReference] string configPath)
{
    // ...
}

// Строка должна быть локализована.
public void SetWindowTitle([LocalizationRequired] string title)
{
    Console.Title = title;
}

// Параметр должен быть именем одного из параметров вызывающего метода.
public void GuardNotNull(object arg, [InvokerParameterName] string paramName)
{
    if (arg == null)
        throw new ArgumentNullException(nameof(paramName));
}


// -------------------------------------------------------------------
// 5. Управление Коллекциями
// -------------------------------------------------------------------

// Как метод, конструктор или свойство влияет на коллекцию внутри.
// - None: никак не влияет;
// - Read: только читает;
// - ModifyExistingContent: изменяет элементы коллекции;
// - UpdatedContent: изменяет состав коллекции.
[CollectionAccess(CollectionAccessType.Read)] 
public void ReadOnlyAccess(IReadOnlyList<int> list)
{
    foreach (var item in list)
        Console.WriteLine(item);
}

// IEnumerable не будет перечислен (MoveNext, foreach, Linq, ...)
public void CheckCountFast([NoEnumeration] IEnumerable items)
{
    if (items is ICollection collection) // без перечисления
        Console.WriteLine(collection.Count);
}

// IEnumerable будет обработан немедленно (NoLazy).
public void ProcessItemsImmediately([InstantHandle] IEnumerable items)
{
    foreach (var item in items)
    {
        // ...
    }
}

// Метод является частью цеопчки Linq-вызовов.
[LinqTunnel]
public static T AndLog<T>(this T obj)
{
    Console.WriteLine(obj);
    return obj;
}

// -------------------------------------------------------------------
// 6. Контроль использования
// -------------------------------------------------------------------

// Подавляет "unused" при неявном использовании
// (рефлексия, DI, сериализация, ...)
[UsedImplicitly]
public class MyApiController
{
    // ...
}

// Помечает кастомный атрибут как [UsedImplicitly].
[MeansImplicitUse(ImplicitUseTargetFlags.WithMembers)]
[AttributeUsage(AttributeTargets.Class)]
public class CustomAttribute : Attribute
{
    // ...
}

// Класс или его методы являются публичным API.
// Подавляет "unsed".
[PublicAPI]
public class MyLibraryFacade
{
    // ...
}

Комментарии (2)


  1. DmitrySharov
    03.11.2025 09:04

    Спасибо за список атрибутов.

    Может быть есть ещё атрибут, который будет работать с JB библиотекой lifetimes.

    А именно интересует как можно пометить ISource<T?> Чтобы ide подсказала, что может прийти null?