Я примерно знаком с шаблоном Dispose для незавершаемых типов, например, типов, которые обертывают какой-то управляемый ресурс, для которого мы хотим выполнить детерминированную очистку. Типы такого рода обычно не реализуют финализатор, так как он совершенно не нужен.
Однако я реализую оболочку C# для собственного API, где я оборачиваю несколько связанных неуправляемых ресурсов и, по-видимому, потребуется несколько классов, каждый из которых реализует шаблон finalizable-dispose. Проблема в том, что в рекомендациях по шаблону удаления говорится, что финализируемый A не должен полагаться на финализируемый B, а это именно то, что мне нужно:
X ЗАПРЕЩАЕТСЯ обращаться к финализируемым объектам в пути кода финализатора, так как существует значительный риск того, что они уже финализированы.
Например, финализируемый объект A, который имеет ссылку на другой финализируемый объект B, не может надежно использовать B в финализаторе A, или наоборот. Финализаторы вызываются в случайном порядке (за исключением слабой гарантии упорядочения для критической финализации).
Итак, вот мои ограничения:
- Чтобы что-то сделать, мне нужно создать дескриптор «API».
- Чтобы создать «дочерние» дескрипторы, я должен предоставить дескриптор API в вызове «создать дочерний».
- Оба типа ручек не могут протекать.
- Если дескриптор API закрыт, все его дочерние дескрипторы неявно закрыты.
- Чтобы закрыть дочерний дескриптор, я должен предоставить дочерний дескриптор и дескриптор API в собственном вызове.
Нативный API выглядит примерно так:
APIHANDLE GizmoCreateHandle();
CHILDHANDLE GizmoCreateChildHandle( APIHANDLE apiHandle );
GizmoCloseHandle( APIHANDLE apiHandle );
GizmoCloseChildHandle( APIHANDLE apiHandle, CHILDHANDLE childHandle);
Наивный подход для этого будет состоять из двух частей:
- Для дескриптора API следуйте типичному шаблону SafeHandle.
- Для дочернего дескриптора следуйте типичному шаблону SafeHandle, но немного измените его, поскольку дочернему дескриптору требуется ссылка на дескриптор API, чтобы реализовать его переопределение ReleaseHandle — добавьте метод, который дает дескриптор API дочернему SafeHandle после он был построен.
Таким образом, все будет выглядеть примерно так:
[DllImport( "gizmo.dll" )]
private static extern ApiSafeHandle GizmoCreateHandle();
[DllImport( "gizmo.dll" )]
private static extern void GizmoCloseHandle( IntPtr apiHandle );
[DllImport( "gizmo.dll" )]
private static extern ChildSafeHandle GizmoCreateChildHandle(ApiSafeHandle apiHandle);
[DllImport( "gizmo.dll" )]
private static extern void GizmoCloseChildHandle( ApiSafeHandle apiHandle, IntPtr childHandle );
[DllImport( "gizmo.dll" )]
private static extern void GizmoChildModify( ChildSafeHandle childHandle, int flag );
public class ApiSafeHandle : SafeHandle
{
public ApiSafeHandle() : base( IntPtr.Zero, true ) { }
public override bool IsInvalid
{
get { return this.handle == IntPtr.Zero; }
}
protected override bool ReleaseHandle()
{
GizmoCloseHandle( this.handle );
return true;
}
}
public class ChildSafeHandle : SafeHandle
{
private ApiSafeHandle apiHandle;
public ChildSafeHandle() : base( IntPtr.Zero, true ) { }
public override bool IsInvalid
{
get { return this.handle == IntPtr.Zero; }
}
public void SetParent( ApiSafeHandle handle )
{
this.apiHandle = handle;
}
// This method is part of the finalizer for SafeHandle.
// It access its own handle plus the API handle, which is also a SafeHandle
// According to MSDN, this violates the rules for finalizers.
protected override bool ReleaseHandle()
{
if ( this.apiHandle == null )
{
// We were used incorrectly - we were allocated, but never given
// the means to deallocate ourselves
return false;
}
else if ( this.apiHandle.IsClosed )
{
// Our parent was already closed, which means we were implicitly closed.
return true;
}
else
{
GizmoCloseChildHandle( apiHandle, this.handle );
return true;
}
}
}
public class GizmoApi
{
ApiSafeHandle apiHandle;
public GizmoApi()
{
this.apiHandle = GizmoCreateHandle();
}
public GizmoChild CreateChild()
{
ChildSafeHandle childHandle = GizmoCreateChildHandle( this.apiHandle );
childHandle.SetParent( this.apiHandle );
return new GizmoChild( childHandle );
}
}
public class GizmoChild
{
private ChildSafeHandle childHandle;
internal GizmoChild( ChildSafeHandle handle )
{
this.childHandle = handle;
}
public void SetFlags( int flags )
{
GizmoChildModify( this.childHandle, flags );
}
// etc.
}
Однако теперь у меня есть недостаток — ReleaseHandle моего ChildSafeHandle ссылается на другой дескриптор для выполнения своей работы. Теперь у меня есть два финализируемых одноразовых объекта, где финализатор одного зависит от другого. MSDN прямо говорит, что финализаторы не должны полагаться на другие финализируемые объекты, поскольку они могут быть уже финализированы, или, если .Net когда-либо будет поддерживать многопоточную финализацию, они могут финализироваться одновременно.
Как правильно это сделать? Позволяют ли мне правила получить доступ к завершаемому объекту A из финализатора B, если я сначала проверяю его на достоверность? MSDN не ясно об этом.