2016-07-26 6 views
4

У меня есть класс, который является простой оболочкой для WNetUseConnectionGC.KeepAlive сохранить контекст

Вот реализация (только для справки):

internal class RemoteFileSystemContext : IDisposable 
{ 
    private readonly string _remoteUnc; 
    private bool _isConnected; 
    public RemoteFileSystemContext(string remoteUnc, string username, string password, bool promptUser) 
    { 
     if (WindowsNetworking.TryConnectToRemote(remoteUnc, username, password, promptUser)) 
     { 
      _isConnected = true; 
      _remoteUnc = remoteUnc; 
     } 
     else 
     { 
      GC.SuppressFinalize(this); 
     } 
    } 

    public void Dispose() 
    { 
     Dispose(true); 
    } 

    ~RemoteFileSystemContext() 
    { 
     Dispose(false); 
    } 

    private void Dispose(bool isDisposing) 
    { 
     if (!_isConnected) 
      return; 
     _isConnected = false; 
     if (isDisposing) 
     { 
      GC.SuppressFinalize(this); 
     } 
     WindowsNetworking.DisconnectRemote(_remoteUnc); 
    } 
} 

и здесь использование:

using (var context = WindowsNetworking.CreateRemoteContext(storagePath, login, pass)) 
{ 
    // do something with storagePath 
    GC.KeepAlive(context); 
} 

Вопрос в том, следует ли мне написать GC.KeepAlive(context) или нет? Я имею в виду, что я не писал такой код, пока не прочитал статью (около AsyncLock, но теперь я не могу найти ссылку), и теперь я не уверен, что GC может вызвать финализатор до завершения этого метода. Теоретически, он должен использовать Dispose в finally разделе using, но эта статья была написана умным парнем, поэтому я не уверен сейчас.


Только в случае, я предоставить код для ссылочного класса:

public static class WindowsNetworking 
{ 
    public static bool TryConnectToRemote(string remoteUnc, string username, string password, bool promptUser = false) 
    { 
     bool isUnc = remoteUnc != null && remoteUnc.Length >= 2 && remoteUnc[0] == '\\' && remoteUnc[1] == '\\'; 
     if (!isUnc) 
     { 
      return false; 
     } 
     ConnectToRemote(remoteUnc, username, password, promptUser); 
     return true; 
    } 

    public static IDisposable CreateRemoteContext(string remoteUnc, string username, string password, bool promptUser = false) 
    { 
     return new RemoteFileSystemContext(remoteUnc, username, password, promptUser); 
    } 

    public static void DisconnectRemote(string remoteUNC) 
    { 
     var ret = (NetworkError) WNetCancelConnection2(remoteUNC, CONNECT_UPDATE_PROFILE, false); 
     if (ret != NetworkError.NO_ERROR) 
     { 
      throw new Win32Exception((int) ret, ret.ToString()); 
     } 
    } 

    [DllImport("Mpr.dll")] 
    private static extern int WNetUseConnection(
     IntPtr hwndOwner, 
     NETRESOURCE lpNetResource, 
     string lpPassword, 
     string lpUserID, 
     int dwFlags, 
     string lpAccessName, 
     string lpBufferSize, 
     string lpResult 
     ); 

    [DllImport("Mpr.dll")] 
    private static extern int WNetCancelConnection2(
     string lpName, 
     int dwFlags, 
     bool fForce 
     ); 

    [StructLayout(LayoutKind.Sequential)] 
    private class NETRESOURCE 
    { 
     public int dwScope = 0; 
     public int dwType = 0; 
     public int dwDisplayType = 0; 
     public int dwUsage = 0; 
     public string lpLocalName = ""; 
     public string lpRemoteName = ""; 
     public string lpComment = ""; 
     public string lpProvider = ""; 
    } 

    private static void ConnectToRemote(string remoteUNC, string username, string password, bool promptUser) 
    { 
     NETRESOURCE nr = new NETRESOURCE 
     { 
      dwType = RESOURCETYPE_DISK, 
      lpRemoteName = remoteUNC 
     }; 

     NetworkError ret; 
     if (promptUser) 
      ret = (NetworkError) WNetUseConnection(IntPtr.Zero, nr, "", "", CONNECT_INTERACTIVE | CONNECT_PROMPT, null, null, null); 
     else 
      ret = (NetworkError) WNetUseConnection(IntPtr.Zero, nr, password, username, 0, null, null, null); 

     if (ret != NetworkError.NO_ERROR) 
     { 
      throw new Win32Exception((int) ret, ret.ToString()); 
     } 
    } 
} 
+0

Я бы сказал, нет. Без этой статьи трудно сказать, на что этот «умный парень» на самом деле делал, и правильно ли вы его интерпретировали. –

+0

Это просто история для «оттуда». Вопрос не о каком-то парне, а о завершении. –

+2

Могу сказать: «Вам не нужен« GC.KeepAlive »здесь». Я мог бы даже опубликовать его в качестве ответа, я тоже очень умный парень (и скромный!), И я даже могу быть убедителен.Проблема в том, что я не знаю, в какой точке я спорю * против *, потому что вы не можете ссылаться на эту статью или говорить, почему вы считаете, что она может применяться в этом контексте. –

ответ

4

GC.KeepAlive способ empty. Все, что он делает, это убедиться, что определенная переменная считывается с этой точки кода, потому что в противном случае эта переменная никогда не будет читаться снова и, следовательно, не является допустимой ссылкой для сохранения объекта в живых.

Это бессмысленно, потому что здесь та же переменная, что вы передаете в KeepAliveявляется чтение из снова на более поздний момент времени - во время скрытого finally блока, когда Dispose называется. Таким образом, GC.KeepAlive ничего не дает.

+1

Технически, если 'Dispose' не имеет доступа к каким-либо полям или иным образом использует неявную ссылку, это неверно (если среда выполнения может доказать, что' Dispose' не нуждается в ссылке на объект, она может вернуть ее перед выполнением этого кода), но если он ссылается на объект, то это точно. – Servy

+1

Если 'Dispose' фактически не использует экземпляр объекта, то из целей GC он не продлевает срок жизни объекта; 'GC.KeepAlive' будет. Поэтому в этом конкретном случае это актуально. – Servy

+0

Это ИМХО является причиной того, что 'GC.SuppressFinalize (this)' внутри 'Dispose' даже на объектах, которые не имеют финализаторов, - чтобы гарантировать, что все вложенные объекты, которые * делают *, имеют финализаторы, сохраняются до тех пор, пока' Dispose' закончен. – supercat

0

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

using System; 

namespace SandboxConsole 
{ 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      using (var context = new TestClass()) 
      { 

       GC.Collect(); 
       GC.WaitForPendingFinalizers(); 
       GC.Collect(); 

       Console.WriteLine("After collection"); 
      } 
      Console.WriteLine("After dispose, before 2nd collection"); 

      GC.Collect(); 
      GC.WaitForPendingFinalizers(); 
      GC.Collect(); 

      Console.WriteLine("After 2nd collection"); 
      Console.ReadLine(); 
     } 
    } 

    internal class TestClass : IDisposable 
    { 
     public void Dispose() 
     { 
      Dispose(true); 
     } 

     ~TestClass() 
     { 
      Console.WriteLine("In finalizer"); 
      Dispose(false); 
     } 

     private void Dispose(bool isDisposing) 
     { 
      Console.WriteLine("In Dispose: {0}", isDisposing); 
      if (isDisposing) 
      { 
       //uncomment this line out to have the finalizer never run 
       //GC.SuppressFinalize(this); 
      } 
     } 
    } 
} 

Он будет всегда выводится

 
After collection 
In Dispose: True 
After dispose, before 2nd collection 
In finalizer 
In Dispose: False 
After 2nd collection 

Для более конкретного доказательства, вот IL для главного метода приведенных выше программ

.method private hidebysig static void Main(string[] args) cil managed 
{ 
    .entrypoint 
    // Code size  85 (0x55) 
    .maxstack 1 
    .locals init ([0] class SandboxConsole.TestClass context) 
    IL_0000: newobj  instance void SandboxConsole.TestClass::.ctor() 
    IL_0005: stloc.0 
    .try 
    { 
    IL_0006: call  void [mscorlib]System.GC::Collect() 
    IL_000b: call  void [mscorlib]System.GC::WaitForPendingFinalizers() 
    IL_0010: call  void [mscorlib]System.GC::Collect() 
    IL_0015: ldstr  "After collection" 
    IL_001a: call  void [mscorlib]System.Console::WriteLine(string) 
    IL_001f: leave.s IL_002b 
    } // end .try 
    finally 
    { 
    IL_0021: ldloc.0 
    IL_0022: brfalse.s IL_002a 
    IL_0024: ldloc.0 
    IL_0025: callvirt instance void [mscorlib]System.IDisposable::Dispose() 
    IL_002a: endfinally 
    } // end handler 
    IL_002b: ldstr  "After dispose, before 2nd collection" 
    IL_0030: call  void [mscorlib]System.Console::WriteLine(string) 
    IL_0035: call  void [mscorlib]System.GC::Collect() 
    IL_003a: call  void [mscorlib]System.GC::WaitForPendingFinalizers() 
    IL_003f: call  void [mscorlib]System.GC::Collect() 
    IL_0044: ldstr  "After 2nd collection" 
    IL_0049: call  void [mscorlib]System.Console::WriteLine(string) 
    IL_004e: call  string [mscorlib]System.Console::ReadLine() 
    IL_0053: pop 
    IL_0054: ret 
} // end of method Program::Main 

Вы можете видеть, что есть скрытый наконец, блок, который проверяет, является ли объект нулевым, затем вызывает Dispose на нем. Эта ссылка позволит объекту сохранить всю область использования блока.

UPDATE: См Damien's comment below, это конкретный пример имеет возможность реально назвать финализатор рано из-за того, я никогда не использовать какие-либо переменные, которые используют неявную this в методе Dispose. Чтобы гарантировать поведение, обязательно используйте переменную уровня экземпляра (в моем коротком примере нет) или GC.SuppressFinalize(this); раскоментирован.

+0

Хороший пример фундаментального кода, мне он нравится! – BolletuH

+3

Умная комбинация GC/JIT * может * действительно иметь возможность завершить этот объект раньше, чем явный 'Dispose' - если он может оптимизировать несколько методов, ничто в вашем' TestClass' не ссылается ни на какие поля класса, и поэтому 'this 'ссылки не требуются ни одним из методов. В отличие от класса OPs, который выполняет доступ к локальным полям в 'Dispose'. Так что это наблюдение, а не * гарантия *. –

+0

@Damien_The_Unbeliever Действительно, я не знал, что JIT может зайти так далеко. Спасибо за слово предупреждения. –