Использование классов

Узнайте, как использовать обертки классов для более чистых API плагинов, основанных на объектно-ориентированном программировании, в C#.

Классы в Plugify предоставляют объектно-ориентированный способ работы с ресурсами плагинов, обеспечивая автоматическое управление жизненным циклом и более чистые API. Вместо ручного управления дескрипторами и вызова функций с явными указателями, вы можете использовать интуитивные интерфейсы на основе классов с паттерном IDisposable.

Зачем использовать классы?

Работа с API на основе дескрипторов в традиционном стиле C может быть многословной и подверженной ошибкам:

Без классов (стиль C)
using test_keyvalues;

void ProcessConfig()
{
    // Создать дескриптор KeyValues вручную
    var kvHandle = test_keyvalues.Kv1Create("MyConfig");

    // Установить свойства, используя дескриптор
    test_keyvalues.Kv1SetName(kvHandle, "ServerSettings");

    // Найти подключ
    var subkeyHandle = test_keyvalues.Kv1FindKey(kvHandle, "Players");

    // Легко забыть очистку!
    test_keyvalues.Kv1Destroy(subkeyHandle);
    test_keyvalues.Kv1Destroy(kvHandle);
}

С классами тот же код становится намного чище и безопаснее:

С классами (стиль ООП)
using test_keyvalues;

void ProcessConfig()
{
    // Создать с использованием конструктора
    using var kv = new KeyValues("MyConfig");  // Автоматическая очистка при выходе из области видимости

    // Использовать интуитивные методы
    kv.SetName("ServerSettings");

    // Найти подключ - возвращает экземпляр KeyValues
    using var subkey = kv.FindKey("Players");

    // Автоматическая очистка через оператор using!
}

Преимущества:

  • Более чистый синтаксис - Методы вместо функций с явными дескрипторами
  • Автоматическое управление ресурсами - Паттерн IDisposable с детерминированной очисткой
  • Оператор using - Гарантирует очистку даже при возникновении исключений
  • Безопасность типов - Лучшее автодополнение в IDE и проверки во время компиляции
  • Потокобезопасность - SafeHandle использует DangerousAddRef/DangerousRelease для потокобезопасности
  • Меньше ошибок - Труднее забыть очистку или использовать после освобождения
  • Критическая финализация - Финализаторы выполняются даже при выгрузке AppDomain
  • Безопасность исключений - ObjectDisposedException при использовании после освобождения

Важный паттерн:

Как работают классы

Когда плагин определяет классы в своем манифесте, Plugify автоматически генерирует обертки классов C#, которые:

  1. Наследуются от SafeHandle - Для классов с деструкторами обеспечивает критическую финализацию
  2. Реализуют IDisposable - Позволяет использовать оператор using для детерминированной очистки
  3. Управляют жизненным циклом - Автоматически вызывают деструктор при освобождении или финализации
  4. Предоставляют утилитарные методы - Get(), Release(), Reset(), IsValid для управления дескриптором
  5. Отслеживают владение - Используют перечисление Ownership для предотвращения ошибок двойного освобождения
  6. Потокобезопасные операции - Используют DangerousAddRef/DangerousRelease внутренне
  7. Валидируют дескрипторы - Выбрасывают ObjectDisposedException при использовании после освобождения
  8. Критические финализаторы - Выполняются даже при выгрузке AppDomain

Определение классов в вашем манифесте

Чтобы создать классы для вашего плагина, добавьте секцию classes в ваш манифест:

plugin.pplugin
{
  "name": "example_plugin",
  "version": "1.0.0",
  "language": "cpp",
  "methods": [
    {
      "name": "Kv1Create",
      "funcName": "Kv1Create",
      "paramTypes": [
        { "name": "setName", "type": "string" }
      ],
      "retType": { "type": "ptr64" }
    },
    {
      "name": "Kv1Destroy",
      "funcName": "Kv1Destroy",
      "paramTypes": [
        { "name": "kv", "type": "ptr64" }
      ],
      "retType": { "type": "void" }
    },
    {
      "name": "Kv1GetName",
      "funcName": "Kv1GetName",
      "paramTypes": [
        { "name": "kv", "type": "ptr64" }
      ],
      "retType": { "type": "string" }
    },
    {
      "name": "Kv1SetName",
      "funcName": "Kv1SetName",
      "paramTypes": [
        { "name": "kv", "type": "ptr64" },
        { "name": "name", "type": "string" }
      ],
      "retType": { "type": "void" }
    }
  ],
  "classes": [
    {
      "name": "KeyValues",
      "description": "RAII wrapper for KeyValues handle",
      "handleType": "ptr64",
      "invalidValue": "0",
      "constructors": ["Kv1Create"],
      "destructor": "Kv1Destroy",
      "bindings": [
        {
          "name": "GetName",
          "method": "Kv1GetName",
          "bindSelf": true
        },
        {
          "name": "SetName",
          "method": "Kv1SetName",
          "bindSelf": true
        }
      ]
    }
  ]
}

Объяснение ключевых полей:

  • name: Имя класса C# (требуется PascalCase)
  • handleType: Тип базового дескриптора (обычно ptr64 или ptr32)
  • invalidValue: Какое значение представляет недопустимый дескриптор (обычно "0" или "-1")
  • constructors: Массив имен методов, которые создают экземпляры
  • destructor: Имя метода, который очищает ресурсы (опционально, включает SafeHandle)
  • bindings: Массив методов, доступных в классе
    • bindSelf: Если true, автоматически передает дескриптор в качестве первого параметра

Сгенерированный код C#

Когда вы определяете классы в своем манифесте, Plugify генерирует обертки классов C# с SafeHandle:

Сгенерированный класс (концептуально)
using System;
using System.Runtime.InteropServices;

namespace test_keyvalues
{
    internal enum Ownership { Borrowed, Owned }

    /// <summary>
    /// RAII обертка для дескриптора KeyValues.
    /// </summary>
    internal sealed unsafe class KeyValues : SafeHandle
    {
        /// <summary>
        /// Создает новый экземпляр KeyValues
        /// </summary>
        public KeyValues(string setName)
            : this(test_keyvalues.Kv1Create(setName), Ownership.Owned)
        {
        }

        /// <summary>
        /// Внутренний конструктор для создания KeyValues из существующего дескриптора
        /// </summary>
        private KeyValues(nint handle, Ownership ownership)
            : base(handle, ownsHandle: ownership == Ownership.Owned)
        {
        }

        /// <summary>
        /// Освобождает дескриптор (вызывается автоматически SafeHandle)
        /// </summary>
        protected override bool ReleaseHandle()
        {
            test_keyvalues.Kv1Destroy(handle);
            return true;
        }

        /// <summary>
        /// Проверяет, имеет ли KeyValues действительный дескриптор
        /// </summary>
        public override bool IsInvalid => handle == nint.Zero;

        /// <summary>
        /// Получает базовый дескриптор
        /// </summary>
        public nint Handle => (nint)handle;

        /// <summary>
        /// Проверяет, является ли дескриптор действительным
        /// </summary>
        public bool IsValid => handle != nint.Zero;

        /// <summary>
        /// Получает базовый дескриптор
        /// </summary>
        public nint Get() => (nint)handle;

        /// <summary>
        /// Освобождает владение дескриптором и возвращает его
        /// </summary>
        public nint Release()
        {
            var h = handle;
            SetHandleAsInvalid();
            return h;
        }

        /// <summary>
        /// Освобождает дескриптор
        /// </summary>
        public void Reset()
        {
            Dispose();
        }

        // Связанные методы с потокобезопасным доступом к дескриптору
        /// <summary>
        /// Получает имя секции экземпляра KeyValues
        /// </summary>
        public string GetName()
        {
            ObjectDisposedException.ThrowIf(!IsValid, this);
            bool success = false;
            DangerousAddRef(ref success);
            try
            {
                return test_keyvalues.Kv1GetName(handle);
            }
            finally
            {
                if (success) DangerousRelease();
            }
        }

        /// <summary>
        /// Устанавливает имя секции экземпляра KeyValues
        /// </summary>
        public void SetName(string name)
        {
            ObjectDisposedException.ThrowIf(!IsValid, this);
            bool success = false;
            DangerousAddRef(ref success);
            try
            {
                test_keyvalues.Kv1SetName(handle, name);
            }
            finally
            {
                if (success) DangerousRelease();
            }
        }

        /// <summary>
        /// Находит ключ по имени
        /// </summary>
        public KeyValues FindKey(string keyName)
        {
            ObjectDisposedException.ThrowIf(!IsValid, this);
            bool success = false;
            DangerousAddRef(ref success);
            try
            {
                return new KeyValues(test_keyvalues.Kv1FindKey(handle, keyName), Ownership.Borrowed);
            }
            finally
            {
                if (success) DangerousRelease();
            }
        }

        /// <summary>
        /// Добавляет подключ к этому экземпляру KeyValues
        /// </summary>
        public void AddSubKey(KeyValues subKey)
        {
            ObjectDisposedException.ThrowIf(!IsValid, this);
            bool success = false;
            DangerousAddRef(ref success);
            try
            {
                test_keyvalues.Kv1AddSubKey(handle, subKey.Release());
            }
            finally
            {
                if (success) DangerousRelease();
            }
        }
    }
}

Встроенные утилитарные методы и свойства

Каждый сгенерированный класс включает несколько утилитарных методов для управления дескриптором:

IsValid - Проверка действительности дескриптора

Возвращает true, если дескриптор действителен (не равен invalidValue):

Использование IsValid
using test_keyvalues;

void CheckHandle()
{
    using var kv = new KeyValues("Config");

    if (kv.IsValid)
    {
        kv.SetName("ServerConfig");
        Console.WriteLine("Handle is valid");
    }
    else
    {
        Console.WriteLine("Handle is invalid");
    }

    // После release дескриптор становится недействительным
    var handle = kv.Release();
    Console.WriteLine(kv.IsValid);  // false
}

Get() / Handle - Доступ к необработанному дескриптору

Возвращает значение базового дескриптора. Используйте это, когда вам нужно передать необработанный дескриптор в функции в стиле C:

Использование Get()
using test_keyvalues;

void GetRawHandle()
{
    using var kv = new KeyValues("Config");

    // Получить необработанный дескриптор
    nint rawHandle = kv.Get();
    // или
    nint handle = kv.Handle;

    Console.WriteLine($"Handle value: {rawHandle}");

    // Передать в функцию, ожидающую необработанный дескриптор
    SomeCFunction(kv.Get());
}

Release() - Передача владения

Освобождает владение дескриптором и возвращает его. После вызова Release() экземпляр класса становится недействительным и не будет вызывать деструктор:

Использование Release()
using test_keyvalues;

nint CreateAndRelease()
{
    var kv = new KeyValues("Config");
    kv.SetName("ServerConfig");

    // Передать владение наружу
    nint handle = kv.Release();

    // kv теперь недействителен, не будет очищать
    Console.WriteLine(kv.IsValid);  // false

    // using не нужен - мы освободили владение
    return handle;
}

void UseReleased()
{
    // Теперь мы владеем дескриптором и должны очистить его вручную
    nint rawHandle = CreateAndRelease();
    // ... использовать rawHandle ...
    test_keyvalues.Kv1Destroy(rawHandle);  // Требуется ручная очистка!
}

Используйте Release(), когда вам нужно:

  • Передать владение другой системе
  • Сохранить дескриптор в долгоживущей структуре данных
  • Взаимодействовать с кодом в стиле C, который принимает владение

Reset() / Dispose() - Ручная очистка

Явно освобождает дескриптор. Reset() вызывает Dispose():

Использование Reset() и Dispose()
using test_keyvalues;

void ManualCleanup()
{
    var kv = new KeyValues("Config");
    kv.SetName("ServerConfig");

    // Вариант 1: Явно сбросить
    kv.Reset();

    // Вариант 2: Явно освободить
    var kv2 = new KeyValues("Config2");
    kv2.Dispose();

    // Дескриптор теперь недействителен
    Console.WriteLine(kv.IsValid);  // false

    // Методы выбросят ObjectDisposedException
    try
    {
        kv.SetName("Test");
    }
    catch (ObjectDisposedException ex)
    {
        Console.WriteLine($"Error: {ex.Message}");
    }
}

Метод Dispose():

  • Идемпотентный - Безопасно вызывать несколько раз
  • Вызывается автоматически - С помощью оператора using и финализатора
  • Учитывает владение - Вызывает деструктор только если объект владеет дескриптором
  • Потокобезопасный - Использует потокобезопасное освобождение SafeHandle

Управление ресурсами

Классы C#, сгенерированные Plugify, поддерживают детерминированную очистку через оператор using:

Оператор Using (рекомендуется)

Оператор using обеспечивает детерминированную очистку и является рекомендуемым паттерном:

Оператор Using (рекомендуется)
using test_keyvalues;

void ProcessConfig()
{
    using var kv = new KeyValues("Config");  // C# 8.0+ объявление using
    kv.SetName("ServerConfig");
    kv.SetString("hostname", "My Server");
    // ... использовать kv ...

    // Автоматически освобождается при выходе из области видимости
}

// Или традиционный блок using
void ProcessConfigTraditional()
{
    using (var kv = new KeyValues("Config"))
    {
        kv.SetName("ServerConfig");
        // ... использовать kv ...
    } // Освобождается здесь
}

Это рекомендуемый паттерн, потому что:

  • Гарантированная очистка даже при возникновении исключений
  • Четкая область жизни ресурса
  • Детерминированное время очистки
  • Идиоматичный код C#
  • Работает с критическим финализатором SafeHandle

Критическая финализация (недетерминированный резерв)

SafeHandle обеспечивает критическую финализацию, которая выполняется даже при выгрузке AppDomain:

Критическая финализация
using test_keyvalues;

void ProcessConfig()
{
    var kv = new KeyValues("Config");
    kv.SetName("ServerConfig");
    // ... использовать kv ...
    // НЕТ using - полагается на финализацию (НЕ РЕКОМЕНДУЕТСЯ!)
}

// kv в конечном итоге будет завершен и очищен
// но время непредсказуемо - ИСПОЛЬЗУЙТЕ 'using' ВМЕСТО ЭТОГО!

Случай без деструктора

Если в манифесте класс не имеет определенного деструктора, он генерируется как простой класс-обертка без SafeHandle:

Обертка без деструктора
using test_keyvalues;

void UseWrapper()
{
    // Класс без деструктора - простая обертка, нет IDisposable
    var wrapper = new SomeWrapper();

    // Все еще имеет утилитарные методы
    if (wrapper.IsValid)
    {
        nint handle = wrapper.Get();
        // ... использовать handle ...
    }

    // Нет автоматической очистки - дескриптор сохраняется
    // Полезно для оберток без состояния или глобальных ресурсов
}

Работа с владением

Некоторые методы передают владение ресурсами. Манифест указывает это с помощью поля owner в paramAliases и retAlias.

Принятие владения (параметры метода)

Когда метод принимает владение ресурсом, он автоматически вызывает Release():

Передача владения
using test_keyvalues;

void TransferOwnership()
{
    using var parent = new KeyValues("Parent");
    var child = new KeyValues("Child");
    // НЕ используйте 'using' с child - parent примет владение

    // AddSubKey принимает владение child
    parent.AddSubKey(child);

    // child теперь принадлежит parent
    // child.IsValid теперь false (Release() был вызван внутренне)
    // parent обработает очистку
}

В манифесте это определяется как:

Владение в манифесте
{
  "name": "AddSubKey",
  "method": "Kv1AddSubKey",
  "bindSelf": true,
  "paramAliases": [
    {
      "name": "subKey",
      "owner": true  // Этот параметр принимает владение
    }
  ]
}

Лучшая практика: После передачи владения объект автоматически освобождается:

Правильная обработка владения
using test_keyvalues;

void ProperOwnership()
{
    using var parent = new KeyValues("Parent");
    var child = new KeyValues("Child");

    // Передать владение - child.Release() вызывается внутренне
    parent.AddSubKey(child);

    // child.IsValid теперь false
    // Использовать только через parent
    using var found = parent.FindKey("Child");
}

Возврат владения (возвращаемые значения)

Когда метод возвращает новый ресурс с владением:

Возврат владения
using test_keyvalues;

void ReturnOwnership()
{
    using var parent = new KeyValues("Parent");

    // FindKey возвращает НОВЫЙ KeyValues, которым мы владеем
    using var child = parent.FindKey("Settings");

    if (child.IsValid)
    {
        child.SetName("UpdatedSettings");
        // Мы ответственны за жизненный цикл child
        // Освобождается автоматически с помощью using
    }
}

В манифесте:

Возврат владения в манифесте
{
  "name": "FindKey",
  "method": "Kv1FindKey",
  "bindSelf": true,
  "retAlias": {
    "name": "KeyValues",
    "owner": true  // Вызывающий владеет возвращаемым объектом
  }
}

Невладеющие ссылки

Когда owner: false, метод возвращает ссылку без передачи владения:

Невладеющая ссылка
using test_keyvalues;

void NonOwningRef()
{
    using var parent = new KeyValues("Parent");

    // GetFirstSubKey возвращает ссылку, parent все еще владеет ею
    var childRef = parent.GetFirstSubKey();

    if (childRef.IsValid)
    {
        // Используйте ссылку, но НЕ освобождайте или отпускайте ее
        string name = childRef.GetName();
        // childRef будет очищен parent
        // НЕ используйте 'using' с childRef
    }
}

Жизненный цикл плагина и поля

При использовании классов в плагинах вы должны быть осторожны с освобождением во время выгрузки плагина:

Управление полями

Если вы храните экземпляры классов в полях, вы должны освободить их правильно:

plugin.cs
using Plugify;
using test_keyvalues;

public class MyPlugin : Plugin
{
    // Поле - должно быть освобождено!
    private KeyValues? _config;

    public override void PluginStart()
    {
        // Создать экземпляр поля
        _config = new KeyValues("GlobalConfig");
        _config.SetName("ServerSettings");
        Log("Plugin started with global config");
    }

    public override void PluginEnd()
    {
        // КРИТИЧНО: Освободить поля перед выгрузкой плагина
        _config?.Dispose();
        _config = null;
        Log("Plugin ended, global config disposed");
    }
}

Безопасные паттерны

✅ Безопасно: Локальная область видимости с using

Безопасный паттерн
public void OnCommand(string[] args)
{
    // Локальный объект с using - автоматически освобождается
    using var kv = new KeyValues("TempConfig");
    kv.SetName("CommandConfig");
    // ... использовать kv ...
    // Автоматически освобождается при выходе из области видимости - безопасно!
}

✅ Безопасно: Локальные переменные метода

Безопасный паттерн
public void ProcessData()
{
    using var kv = new KeyValues("TempConfig");
    kv.SetName("CommandConfig");
    // ... использовать kv ...
    // Освобождается с помощью using - безопасно!
}

✅ Безопасно: Поле с освобождением

Безопасный паттерн
public class MyPlugin : Plugin
{
    private KeyValues? _pluginConfig;

    public override void PluginStart()
    {
        _pluginConfig = new KeyValues("PluginConfig");
        _pluginConfig.SetName("Settings");
    }

    public override void PluginEnd()
    {
        // Освободить поля
        _pluginConfig?.Dispose();
        _pluginConfig = null;
    }
}

❌ Небезопасно: Поле без освобождения

Небезопасный паттерн
public class MyPlugin : Plugin
{
    // ОПАСНО: Поле без освобождения
    private KeyValues _config = new KeyValues("GlobalConfig");

    public override void PluginStart()
    {
        _config.SetName("ServerSettings");
    }

    public override void PluginEnd()
    {
        // ОТСУТСТВУЕТ: Нет освобождения!
        // Полагается на финализатор - не идеально!
    }
}

❌ Небезопасно: Статическое поле без освобождения

Небезопасный паттерн
public class MyPlugin : Plugin
{
    // ОПАСНО: Статическое поле
    private static Dictionary<string, KeyValues> _cache = new();

    public void CacheConfig(string name)
    {
        _cache[name] = new KeyValues(name);
    }

    public override void PluginEnd()
    {
        // ОТСУТСТВУЕТ: Нет очистки кэша!
        // Должны освободить все кэшированные объекты!
    }
}

Контрольный список очистки

Перед выгрузкой вашего плагина (PluginEnd()), убедитесь:

  1. ✅ Все экземпляры полей явно освобождены или установлены в null
  2. ✅ Все коллекции (Dictionary, List и т.д.), содержащие экземпляры классов, очищены и освобождены
  3. ✅ Статические поля освобождены, если содержат экземпляры классов
  4. ✅ Вы вызвали Dispose() или использовали using для всех принадлежащих объектов

Полный пример: Система конфигурации

Вот полный пример, показывающий, как использовать классы для системы конфигурации:

ConfigManager.cs
using System;
using test_keyvalues;

public class ConfigManager : IDisposable
{
    private KeyValues? _root;

    public ConfigManager(string configName)
    {
        _root = new KeyValues(configName);

        if (!_root.IsValid)
        {
            throw new InvalidOperationException($"Failed to create config: {configName}");
        }
    }

    public void Dispose()
    {
        _root?.Dispose();
        _root = null;
    }

    public void CreateSection(string sectionName)
    {
        if (_root == null || !_root.IsValid)
            throw new ObjectDisposedException(nameof(ConfigManager));

        var section = new KeyValues(sectionName);

        if (!section.IsValid)
        {
            throw new InvalidOperationException($"Failed to create section: {sectionName}");
        }

        _root.AddSubKey(section);
    }

    public KeyValues? GetSection(string sectionName)
    {
        if (_root == null || !_root.IsValid)
            throw new ObjectDisposedException(nameof(ConfigManager));

        return _root.FindKey(sectionName);
    }

    public void SetValue(string sectionName, string key, string value)
    {
        using var section = GetSection(sectionName);
        if (section == null || !section.IsValid)
            throw new InvalidOperationException($"Section not found: {sectionName}");

        section.SetString(key, value);
    }

    public string GetValue(string sectionName, string key, string defaultValue = "")
    {
        using var section = GetSection(sectionName);
        if (section == null || !section.IsValid)
            return defaultValue;

        return section.GetString(key, defaultValue);
    }

    public void Save(string filename)
    {
        if (_root == null || !_root.IsValid)
            throw new ObjectDisposedException(nameof(ConfigManager));

        _root.SaveToFile(filename);
    }

    public void Load(string filename)
    {
        if (_root == null || !_root.IsValid)
            throw new ObjectDisposedException(nameof(ConfigManager));

        _root.LoadFromFile(filename);
    }
}

// Использование
void UseConfigManager()
{
    using var config = new ConfigManager("ServerConfig");

    config.CreateSection("Server");
    config.SetValue("Server", "hostname", "My Server");
    config.SetValue("Server", "maxplayers", "32");

    config.CreateSection("Game");
    config.SetValue("Game", "mode", "competitive");

    config.Save("config.kv");
}

Лучшие практики

  1. Всегда используйте using - Используйте using var (C# 8.0+) или блоки using для всех объектов IDisposable
  2. Освобождайте поля в PluginEnd() - Критично: Явно освобождайте все экземпляры полей перед выгрузкой плагина
  3. Проверяйте null и IsValid - Всегда проверяйте действительность перед использованием объектов
  4. Не using невладеющие ссылки - Не используйте using с объектами, возвращаемыми методами с owner: false
  5. Предпочитайте локальную область видимости - Держите экземпляры классов в локальной области видимости с using, когда возможно
  6. Обрабатывайте ObjectDisposedException - Будьте готовы перехватывать исключения от освобожденных объектов
  7. Уважайте владение - Не используйте объекты после передачи владения (они автоматически освобождаются)
  8. Используйте nullable ссылочные типы - Включите nullable ссылочные типы (C# 8.0+) для лучшей безопасности null
  9. Реализуйте паттерн IDisposable - При создании оберток следуйте паттерну IDisposable правильно
  10. Доверяйте SafeHandle - SafeHandle обеспечивает потокобезопасную и критическую финализацию, но все равно используйте using
  11. Проверяйте возвраты null - Методы могут возвращать null при неудаче

Когда НЕ использовать классы

Классы предназначены для ресурсов, которым требуется управление жизненным циклом. Не определяйте классы для:

  • Функции-утилиты без состояния - Простые функции, которые не управляют ресурсами
  • Функции, возвращающие примитивные значения - Нет необходимости оборачивать простые геттеры
  • Одноразовые операции - Операции, которые не поддерживают состояние
  • Глобальные синглтоны - Ресурсы, которые живут в течение всего времени работы программы (но все равно рассмотрите IDisposable)

Для этих случаев продолжайте использовать обычный API на основе функций.

Устранение неполадок

ObjectDisposedException

Если вы получаете ObjectDisposedException, вы используете объект после его освобождения:

Обработка освобожденного объекта
using test_keyvalues;

void HandleDisposed()
{
    var kv = new KeyValues("Config");
    kv.Dispose();

    // Это выбрасывает ObjectDisposedException
    try
    {
        kv.SetName("Test");
    }
    catch (ObjectDisposedException ex)
    {
        Console.WriteLine($"Error: {ex.Message}");
    }

    // Сначала проверьте IsValid, чтобы избежать исключений
    if (kv.IsValid)
    {
        kv.SetName("Test");
    }
    else
    {
        Console.WriteLine("Handle is disposed!");
    }
}

Недопустимый дескриптор при создании

Если конструктор возвращает недопустимый дескриптор, проверьте базовую функцию C:

Сбой конструктора дескриптора
using test_keyvalues;

void HandleFailure()
{
    var kv = new KeyValues("Config");

    if (!kv.IsValid)
    {
        Console.WriteLine("Failed to create KeyValues");
        kv.Dispose();
        return;
    }

    using (kv)
    {
        // ... использовать kv ...
    }
}

Висячие ссылки

Будьте осторожны с невладеющими ссылками, когда владелец освобождается:

Проблема висячей ссылки
using test_keyvalues;

KeyValues GetChildRef()
{
    using var parent = new KeyValues("Parent");
    var child = parent.GetFirstSubKey();  // Невладеющая ссылка
    return child;  // ПЛОХО: parent будет освобожден!
}

// Это опасно!
void UseDanglingRef()
{
    var childRef = GetChildRef();
    // childRef теперь указывает на освобожденную память
}

// Лучший подход:
KeyValues GetChildOwned()
{
    var parent = new KeyValues("Parent");

    var child = parent.FindKey("Child");  // Возвращает принадлежащий экземпляр
    if (child == null || !child.IsValid)
    {
        parent.Dispose();
        return null;
    }

    // Вызывающий должен освободить и parent, и child
    // Или держать parent живым
    return child;
}

Освобождение при выгрузке плагина

Если вы полагаетесь на финализаторы:

Правильное освобождение
using Plugify;
using test_keyvalues;

public class MyPlugin : Plugin
{
    private KeyValues? _config;
    private Dictionary<string, KeyValues> _cache = new();

    public override void PluginStart()
    {
        _config = new KeyValues("Config");
        _cache["main"] = new KeyValues("Main");
    }

    public override void PluginEnd()
    {
        // Освободить поле
        _config?.Dispose();
        _config = null;

        // Освободить кэшированные объекты
        foreach (var kv in _cache.Values)
        {
            kv.Dispose();
        }
        _cache.Clear();

        Log("All resources disposed safely");
    }
}

См. также

  • Узнайте, как экспортировать свои собственные классы
  • Узнайте об импорте функций
  • Справочник по манифесту плагина