C# 結合 using 語句塊的三種實用方法

1、簡介

閱讀 Abp 源碼的過程當中,本身也學習到了一些以前沒有接觸過的知識。在這裏,我在這兒針對研究學習 Abp 框架中,遇到的一些值得分享的知識寫幾篇文章。若是有什麼疑問或者問題,歡迎你們評論指正。html

在本篇主要是 Scoped 範圍與 using 語句塊的使用。using 語句塊你們必定都不陌生,都是與非託管對象一塊兒存在的,它有一個特性就是在 using 語句塊結束的時候會調用對象的 IDispose.Dispose() 方法。通常咱們會在非託管類型的 Dispose() 方法內部進行資源的釋放,相似於 C 語言的 free() 操做。多線程

例以下面的代碼:框架

public void TestMethod()
{
    using(var waitDisposeObj = new TestClass())
    {
        // 執行其餘操做 xxx
    }
    
    // 出了語句塊以後就,自動調用 waitDisposeObj 的 Dispose() 方法。
}

能夠看到上面的例子,using 語句塊包裹的就是一個範圍 (Scoped)。其實這裏能夠延伸到依賴注入的概念,在依賴注入的生命週期當中有一個 Scoped 的生命週期。(PS: 須要瞭解的能夠去閱讀個人 這篇文章)異步

一個 Scoped 其實就能夠看做是一個 using 語句塊包裹的範圍,全部解析出來的對象在離開 using 語句塊的時候都應該被釋放。async

例以下面的代碼:ide

public void TestMethod()
{
    using(var scopedResolver = new ScopedResolver())
    {
        var a = scopedResolver.Resolve<A>();
        var b = scopedResolver.Reslove<B>();
    }
    
    // 出了語句塊以後 a b 對象自動釋放
}

其實這裏也是利用了 using 語句塊的特性,在 ScopedResolver 類型的定義當中,也實現了 IDisopse 接口。因此在 using 語句塊結束的時候,會自動調用 ScopedResovlerDispose() 方法,在這個方法內部則對已經解析出來的對象調用其 Dispose() 進行釋放。函數

2、分析

2.0 釋放委託

也是不知道叫什麼標題了,這玩意兒是 Abp 封裝的一個類型,它的做用就是在 using 語句塊結束的時候,執行你傳入的委託。學習

使用方法以下:ui

var completedTask = new DisposeAction(()=>Console.WriteLine("using 語句塊結束了。"));
using(completedTask)
{
    // 其餘操做
}
// 執行完成以後會調用 completedTask 傳入的委託。

根據上述用法,你也應該猜出來這個 DisposeAction 類型的定義了。該類型繼承了 IDispose 接口,而且在內部有一個 Action 字段,用於存儲構造函數傳入的委託。在執行 Dispose() 方法的時候,執行傳入的委託。線程

public class DisposeAction : IDisposable
{
    public static readonly DisposeAction Empty = new DisposeAction(null);

    private Action _action;

    public DisposeAction([CanBeNull] Action action)
    {
        _action = action;
    }

    public void Dispose()
    {
        // 防止在多線程環境下,屢次調用 action
        var action = Interlocked.Exchange(ref _action, null);
        action?.Invoke();
    }
}

2.1 統一對象釋放

統一對象釋放是 Abp 當中的另外一種用法,其實按照 Abp 框架的定義,叫作 ScopedResolver(範圍解析器)。顧名思義,經過 ScopedResolver 解析出來的對象,都會在 using 語句塊結束以後統一進行銷燬。

IScopedIocResolver 接口繼承自 IIocResolverIDisposable 接口,它的本質就是做爲 Ioc 解析器的一種特殊實現,因此它擁有全部 Ioc 解析器的方法,這裏就再也不贅述。

它的實現也比較簡單,在其內部有一個集合維護每一次經過 IIocResolver 解析出來的對象。在 Dispose() 方法執行的時候,遍歷這個集合,調用 Ioc 解析器的 Release() 方法釋放對象並從集合中刪除對象。下面就是實現的簡化版:

public class ScopedIocResolver : IScopedIocResolver
{
    private readonly IIocResolver _iocResolver;
    private readonly List<object> _resolvedObjects;

    public ScopedIocResolver(IIocResolver iocResolver)
    {
        _iocResolver = iocResolver;
        _resolvedObjects = new List<object>();
    }
    
    // 解析對象
    public object Resolve(Type type)
    {
        var resolvedObject = _iocResolver.Resolve(type);

        // 添加到集合,方便後續釋放
        _resolvedObjects.Add(resolvedObject);
        return resolvedObject;
    }
    
    public void Release(object obj)
    {
        // 從集合當中移除
        _resolvedObjects.Remove(obj);
        // 經過 Ioc 管理器釋放對象
        _iocResolver.Release(obj);
    }
    
    public void Dispose()
    {
        // 遍歷集合,釋放對象
        _resolvedObjects.ForEach(_iocResolver.Release);
    }
}

經過 IScopedResolver 解析出來的對象,在 using 語句塊結束的時候都會被釋放,免去了咱們每次手動釋放的操做。

2.2 臨時值變動

暫時想不到一個好一點的標題,暫時用這個標題代替吧。這裏以 Abp 的一段實例代碼爲例,在有的時候咱們可能當前的用戶沒有登陸,因此在 IAbpSession 裏面的 UserId 等屬性確定是爲 NULL 的。而 IAbpSession 在設計的時候,這些屬性是不容許更改的。

那麼咱們有時候可能會臨時更改 IAbpSession 裏面關於 UserId 的值怎麼辦呢?

這個時候能夠經過 IAbpSession 提供的一個 IDisposable Use(int tenantId, long? userId, string userCode) 進行臨時更改。他擁有一個 Use() 方法,而且返回一個實現了 IDispose 接口的對象,用法通常是這樣:

public void TestMethod()
{
    using(AbpSession.Use(1,2,"3"))
    {
       // 內部臨時更改了 AbpSession 的值 
    }
    
    // using 語句塊結束的時候,調用 Use 返回對象的 Dispose 方法。
}

轉到其抽象類 AbpSessionBase 實現,能夠看到他的實現是這個樣子的:

protected IAmbientScopeProvider<SessionOverride> SessionOverrideScopeProvider { get; }

public IDisposable Use(int tenantId, long? userId, string userCode)
{
    return SessionOverrideScopeProvider.BeginScope(SessionOverrideContextKey, new SessionOverride(null, tenantId, userId, userCode));
}

因此在這裏,它是經過 SessionOverrideScopeProviderBegionScope() 方法建立了能夠被 Dispose() 的對象。

接着繼續跳轉,來到 IAmbientScopeProvider 接口定義,這個接口接受一個泛型參數,能夠看到以前在 AbpSessionBase 傳入了一個 SessionOverride。這個 SessionOverride 就是封裝了 UserId 等信息的存儲類,也就是說 SessionOverride 就是容許進行臨時值更改的類型定義。

在開始執行 BegionScope() 方法的時候,就針對傳入的 value 進行存儲,獲取 Session 值的時候優先讀取存儲的值,不存在才執行真正的讀取,調用 Dispose() 方法的時候就進行釋放。

因此接口提供了兩個方法,第一個咱們先看 BegionScope() 方法,接收一個 contextKey 用來區分不一樣的臨時值,第二個參數則是要存儲的臨時值。

第二個方法爲 GetValue,從一個上下文(後面講)當中根據 contextKey 得到存儲的臨時值。

public interface IAmbientScopeProvider<T>
{
    T GetValue(string contextKey);

    IDisposable BeginScope(string contextKey, T value);
}

針對於該接口,其默認實現是 DataContextAmbientScopeProvider ,它的內部可能略微複雜,牽扯到了另外一個接口 IAmbientDataContextScopeItem 類型。

這兩個類型一個是上下文,一個是包裹具體臨時值對象的類型。咱們先從 BeginScope() 方法開始看:

// ScopeItem 的 Id 與其值關聯的字典,其鍵爲 Guid,值爲具體的 ScopeItem 對象,這裏並未與 ContextKey 進行關聯。
private static readonly ConcurrentDictionary<string, ScopeItem> ScopeDictionary = new ConcurrentDictionary<string, ScopeItem>();

// 數據的上下文對象,管理 ContextKey 與其 Id。
private readonly IAmbientDataContext _dataContext;

public IDisposable BeginScope(string contextKey, T value)
{
    // 將須要臨時存儲的對象,用 ScopeItem 包裝起來,它的外部對象是當前對象 (若是存在的話)。
    var item = new ScopeItem(value, GetCurrentItem(contextKey));

    // 將包裝好的對象以 Id-對象,的形式存儲在字典當中。
    if (!ScopeDictionary.TryAdd(item.Id, item))
    {
        throw new AbpException("Can not add item! ScopeDictionary.TryAdd returns false!");
    }

    // 在上下文當中設置當前的 ContextKey 關聯的 Id。
    _dataContext.SetData(contextKey, item.Id);

    // 集合釋放委託,using 語句塊結束時,作釋放操做。
    return new DisposeAction(() =>
    {
        // 從字典中移除指定 Id 的對象。
        ScopeDictionary.TryRemove(item.Id, out item);

        // 若是包裝對象沒有外部對象,直接設置上下文關聯的 Id 爲 NULL。
        if (item.Outer == null)
        {
            _dataContext.SetData(contextKey, null);
            return;
        }

        // 若是還有外部對象,則設置上下文關聯的 Id 爲外部對象的 I的。
        _dataContext.SetData(contextKey, item.Outer.Id);
    });
}

從上面的邏輯能夠看出來,每次咱們加入的臨時值都是經過 ScopeItem 包裹起來的。而這個 ScopeItem 與咱們的工做單元類似,它會有一個外部鏈接的對象。這個外部鏈接對象的做用就是解決 using 語句嵌套問題的,例如咱們有如下代碼:

public void TestMethod()
{
    using(AbpSession.Use(1,2,"3"))
    {
       // 一些業務邏輯
       // ScopeItem.Outer = null;
       using(AbpSession.Use(4,5,"6"))
       {
           // 一些業務邏輯
           // ScopeItem.Outer = 外部對象;
       }
    }
}

那麼咱們在這裏會有同一個 ContextKey,都是提供給 AbpSession 使用的。第一次我在 Use() 內部經過 BeginScope() 方法建立了一個 ScopeItem 對象,包裝了臨時值,這個 ScopeItem 的外部對象爲 NULL。第二次我又在內部建立了一個 ScopeItem 對象,包裝了第二個臨時值,這個時候 ScopeItem 的外部對象就是第一次包裝的對象了。

執行釋放操做的時候,首先判斷外部對象是否爲空。若是爲空則直接在上下文當中將綁定的 ScopeItem 的 Id 值設爲 NULL,若是不爲空,則設置爲它的外部對象的 Id。

仍是以上面的代碼爲例,在 Dispose() 被執行以後,由內而外,到最外層的時候在上下文與 ContextKey 關聯的 Id 已經被置爲 NULL 了。

private ScopeItem GetCurrentItem(string contextKey)
{
    // 從數據上下文獲取指定 ContextKey 當前關聯的 Id 值。
    var objKey = _dataContext.GetData(contextKey) as string;
    // 不存在則返回 NULL,存在則嘗試以 Id 從字典中拿取對象外部,並返回。
    return objKey != null ? ScopeDictionary.GetOrDefault(objKey) : null;
}

分析了一下 IAmbientDataContext 的實現,感受與 ICurrentUnitOfWorkProvider 相似,內部都是經過 AsyncLocal 來進行處理的。

public class AsyncLocalAmbientDataContext : IAmbientDataContext, ISingletonDependency
{
    // 這裏的字典是以 ContextKey 與 ScopeItem 的 Id 構成的。
    private static readonly ConcurrentDictionary<string, AsyncLocal<object>> AsyncLocalDictionary = new ConcurrentDictionary<string, AsyncLocal<object>>();

    public void SetData(string key, object value)
    {
        // 設置指定 ContextKey 對應的 Id 值。
        var asyncLocal = AsyncLocalDictionary.GetOrAdd(key, (k) => new AsyncLocal<object>());
        asyncLocal.Value = value;
    }

    public object GetData(string key)
    {
        // 獲取指定 ContextKey 對應的 Id 值。
        var asyncLocal = AsyncLocalDictionary.GetOrAdd(key, (k) => new AsyncLocal<object>());
        return asyncLocal.Value;
    }
}

從開始到這裏使用並行字典的狀況來看,這裏這麼作的緣由很簡單,是爲了處理異步上下文切換的狀況,確保 ContextKey 對應的 Id 是一致的,防止在 Get/Set Data 的時候出現 意外的狀況

最後呢在具體的 Session 實現類 ClaimsAbpSession 當中要獲取 UserId 會通過下面的步驟:

public override long? UserId
{
    get
    {
        // 嘗試從臨時對象中獲取數據。
        if (OverridedValue != null)
        {
            return OverridedValue.UserId;
        }

        // 從 JWT Token 當中獲取 UserId 信息。

        return userId;
    }
}

最後我再貼上 ScopeItem 的定義。

private class ScopeItem
{
    public string Id { get; }

    public ScopeItem Outer { get; }

    public T Value { get; }

    public ScopeItem(T value, ScopeItem outer = null)
    {
        Id = Guid.NewGuid().ToString();

        Value = value;
        Outer = outer;
    }
}

這個臨時值變動多是 Abp 用法當中最爲複雜的一個,牽扯到了異步上下文和 using 語句嵌套的問題。但仔細閱讀源碼以後,其實有一種豁然開朗的感受,也增強了對於 C# 程序設計的理解。

3、結語

經過學習 Abp 框架,也瞭解了本身在基礎方面的諸多不足。其次也是可以看到一些比較實用新奇的寫法,你也能夠在本身項目中進行應用,本文主要是起一個拋磚引玉的做用。最近年末了,事情也比較多,博客也是疏於更新。後面會陸續恢復博文更新,儘可能 2 天 1 更,新年新氣象。

相關文章
相關標籤/搜索