Узнайте, как использовать обертки классов для более чистых 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 при использовании после освобождения
Важный паттерн:
Используйте using для очистки: C# предоставляет оператор using (или объявление) для детерминированной очистки ресурсов. Всегда используйте using с объектами IDisposable, чтобы обеспечить правильную очистку даже при возникновении исключений.
Возвращает 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
}
Возвращает значение базового дескриптора. Используйте это, когда вам нужно передать необработанный дескриптор в функции в стиле 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());
}
Предупреждение: Будьте осторожны при использовании Get() или Handle. Возвращаемый дескриптор все еще принадлежит экземпляру класса и будет уничтожен при освобождении экземпляра.
Освобождает владение дескриптором и возвращает его. После вызова 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()
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
Оператор 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
Лучшая практика: Всегда используйте using с объектами IDisposable. В C# 8.0+ используйте объявления using var для более чистого кода. Объект будет освобожден при выходе из области видимости.
SafeHandle обеспечивает критическую финализацию, которая выполняется даже при выгрузке AppDomain:
Критическая финализация
using test_keyvalues;
void ProcessConfig()
{
var kv = new KeyValues("Config");
kv.SetName("ServerConfig");
// ... использовать kv ...
// НЕТ using - полагается на финализацию (НЕ РЕКОМЕНДУЕТСЯ!)
}
// kv в конечном итоге будет завершен и очищен
// но время непредсказуемо - ИСПОЛЬЗУЙТЕ 'using' ВМЕСТО ЭТОГО!
Важно - Жизненный цикл плагина: Критический финализатор SafeHandle выполняется даже при выгрузке AppDomain, обеспечивая страховку. Однако вы все равно должны использовать правильные паттерны освобождения. Если вы храните объекты в полях, вы несете ответственность за их освобождение в PluginEnd(), иначе поведение может быть неоптимальным.
Примечание: Критические финализаторы в SafeHandle более надежны, чем обычные финализаторы, но время финализации все еще недетерминировано. Всегда используйте using для предсказуемой очистки.
Если в манифесте класс не имеет определенного деструктора, он генерируется как простой класс-обертка без SafeHandle:
Обертка без деструктора
using test_keyvalues;
void UseWrapper()
{
// Класс без деструктора - простая обертка, нет IDisposable
var wrapper = new SomeWrapper();
// Все еще имеет утилитарные методы
if (wrapper.IsValid)
{
nint handle = wrapper.Get();
// ... использовать handle ...
}
// Нет автоматической очистки - дескриптор сохраняется
// Полезно для оберток без состояния или глобальных ресурсов
}
Когда метод принимает владение ресурсом, он автоматически вызывает 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
}
}
Когда 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
}
}
Важно: С невладеющими ссылками не используйте оператор using. Возвращаемый объект все еще действителен (имеет дескриптор), но вы не владеете им. Освобождение родителя сделает эти ссылки недействительными.
Если вы храните экземпляры классов в полях, вы должны освободить их правильно:
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");
}
}
Критично: Всегда освобождайте поля в PluginEnd(). Хотя критический финализатор SafeHandle обеспечивает страховку, явное освобождение все еще важно для предсказуемой очистки ресурсов.
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()
{
// ОТСУТСТВУЕТ: Нет очистки кэша!
// Должны освободить все кэшированные объекты!
}
}
Вот полный пример, показывающий, как использовать классы для системы конфигурации:
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");
}
Если вы получаете 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;
}