簡體   English   中英

如何僅基於使用Roslyn的接口來識別方法實現是否標記為異步/可以異步調用?

[英]How to identify if the method implementation is marked as async / can be called asynchronously, based only on its interface using Roslyn?

背景資料

我正在為Visual Studio構建基於Roslyn的CodeFix,用於處理類未實現接口(或缺少該接口的一部分)的情況。

這些接口通常是第三方代碼,例如Microsoft的IDocumentClient。

然后,我創建該接口的實現,作為對方法和屬性的調用的裝飾化實現的一部分,方法和屬性的調用通過處理來自3個輔助方法中最相關的候選對象的實際執行來“包裝”。 這些幫助程序方法處理不同返回類型的方案,包括空返回,非Task類型和通用Task類型。

輔助方法將調用Polly庫。 在幫助程序返回通用Task類型的情況下,尤其是Polly ExecuteAsync方法,該方法執行傳遞的方法委托,並根據用戶指定的行為(重試,斷路器等)處理異常。

我的項目的代碼可以在Github的Polly.Contrib.Decorator上找到。

問題

我需要能夠通過接口聲明中包含的信息來識別正在創建的方法是否異步。

這將確定兩件事:

  1. 如果我的實現應使用async修飾符標記。

  2. 如果實現可以異步調用,讓我來決定,如果我的實現方法-這是包裹- 可能 ,然后,如果它應該由我封裝代碼異步處理。

我不能依靠任何其他外部信息。

我考慮過的

我已經看過使用方法的返回類型來確定它是否是Task ,但是在某些情況下,即使其實際實現中已用async標記,其接口中的方法聲明也可能是“ returning void”修飾符或可以異步方式調用。

檢查異步后綴的名稱顯然是不可靠的。 並非每個人都遵循這樣的約定。

是否存在一種可靠的方法來標識方法實現是否異步,即是否應使用async進行修飾,並且可以僅使用Roslyn根據其接口聲明對其進行異步處理?

(請參閱評論討論,該討論表明此問題的演變)

Roslyn具有內部的IsAwaitableNonDynamic擴展方法 ,該擴展方法完全IsAwaitableNonDynamic您的需求。

您可以復制它:

    /// <summary>
    /// If the <paramref name="symbol"/> is a method symbol, returns <see langword="true"/> if the method's return type is "awaitable", but not if it's <see langword="dynamic"/>.
    /// If the <paramref name="symbol"/> is a type symbol, returns <see langword="true"/> if that type is "awaitable".
    /// An "awaitable" is any type that exposes a GetAwaiter method which returns a valid "awaiter". This GetAwaiter method may be an instance method or an extension method.
    /// </summary>
    public static bool IsAwaitableNonDynamic(this ISymbol symbol, SemanticModel semanticModel, int position)
    {
        IMethodSymbol methodSymbol = symbol as IMethodSymbol;
        ITypeSymbol typeSymbol = null;

        if (methodSymbol == null)
        {
            typeSymbol = symbol as ITypeSymbol;
            if (typeSymbol == null)
            {
                return false;
            }
        }
        else
        {
            if (methodSymbol.ReturnType == null)
            {
                return false;
            }
        }

        // otherwise: needs valid GetAwaiter
        var potentialGetAwaiters = semanticModel.LookupSymbols(position,
                                                               container: typeSymbol ?? methodSymbol.ReturnType.OriginalDefinition,
                                                               name: WellKnownMemberNames.GetAwaiter,
                                                               includeReducedExtensionMethods: true);
        var getAwaiters = potentialGetAwaiters.OfType<IMethodSymbol>().Where(x => !x.Parameters.Any());
        return getAwaiters.Any(VerifyGetAwaiter);
    }

    private static bool VerifyGetAwaiter(IMethodSymbol getAwaiter)
    {
        var returnType = getAwaiter.ReturnType;
        if (returnType == null)
        {
            return false;
        }

        // bool IsCompleted { get }
        if (!returnType.GetMembers().OfType<IPropertySymbol>().Any(p => p.Name == WellKnownMemberNames.IsCompleted && p.Type.SpecialType == SpecialType.System_Boolean && p.GetMethod != null))
        {
            return false;
        }

        var methods = returnType.GetMembers().OfType<IMethodSymbol>();

        // NOTE: (vladres) The current version of C# Spec, §7.7.7.3 'Runtime evaluation of await expressions', requires that
        // NOTE: the interface method INotifyCompletion.OnCompleted or ICriticalNotifyCompletion.UnsafeOnCompleted is invoked
        // NOTE: (rather than any OnCompleted method conforming to a certain pattern).
        // NOTE: Should this code be updated to match the spec?

        // void OnCompleted(Action) 
        // Actions are delegates, so we'll just check for delegates.
        if (!methods.Any(x => x.Name == WellKnownMemberNames.OnCompleted && x.ReturnsVoid && x.Parameters.Length == 1 && x.Parameters.First().Type.TypeKind == TypeKind.Delegate))
        {
            return false;
        }

        // void GetResult() || T GetResult()
        return methods.Any(m => m.Name == WellKnownMemberNames.GetResult && !m.Parameters.Any());
    }

TL; DR

潛在的問題是,在接口中包裝方法的庫應如何調用這些方法-是否await 不僅在實現上是否聲明了async (從接口,在調用站點的上下文之外)是否不可檢測,而且為此目的還不確定。

  • (1) async關鍵字是否出現在方法實現中並不能充分確定對其的調用是否可以使用或應該使用await
  • (2)一種方法, 可以 await ED如果且僅-如果返回類型await能夠(在此被充分確定)。
  • (3) async void方法不會改變以上結論; async void方法不會異步運行,因為您不能使用await來調用它。

(1) async關鍵字是否出現在方法實現中並不能充分確定對其的調用是否可以使用或應該使用await

async不是方法簽名的正式形式。 因此,在接口中找不到async 因此,您不能從接口確定接口的原始作者是否打算使用async關鍵字編寫方法實現或使用await調用方法實現。

但是,是否使用async關鍵字編寫了被調用的方法實際上並不是確定(/甚至是足夠確定的)因素,因為方法是否可以/應該使用await來調用。 在沒有async關鍵字的情況下,有寫方法的有效情況會返回await

[a] async-await eilision由Polly廣泛使用
[b]主要用於I / O上的實現的接口,因此使用await返回類型進行聲明,但有時您可能還想為其編寫內存中(如此同步)的實現/攔截器。 常見情況的討論:出於測試目的, 圍繞異步I / O同步內存中的緩存 [c],使用內存中的(存根)存根來存根一些異步依賴性

async不是方法簽名的一部分,這是很好的選擇,因為async允許我們偶爾實現上述的同步實現。)

(2)一個方法只有在其返回類型是可等待的時才可以等待(這已足夠確定); 即返回類型為TaskTask<TResult>具有合適的GetAwaiter()方法

唯一確定是否可以 await方法調用的唯一事情是其返回類型是否可以await

(3) async void方法不會改變以上結論

這解決了async void ,因為該方法的返回類型可能是不夠的,大概是因為在問題的意見的篇幅方法, void不能區別於(接口) async void

起點是不能等待async void方法,原因很簡單,盡管它們使用async關鍵字編寫,但它們不返回可以await任何類型。

這是否會使我們的結論(2)無效? (我們可以唯一地使用方法是否返回一個await從而確定如何調用它)。 我們是否因為無法await 異步async void方法而失去了一些能力

具體地說:說接口背后的方法是async void Foo()但我們從接口了解到的只是它是void Foo() 如果僅調用Foo()我們是否失去了Foo()異步運行的功能?

答案是否定的,因為async方法的運行方式。 async void方法的行為就像使用await調用的任何async Task/Task<T>方法一樣:它們同步運行,直到第一個內部await為止; 然后,他們返回( void ,或表示完成Task ),並調度被調用方法的其余部分(第一次await之后的部分)作為繼續。 await事情完成之后,該連續部分將異步運行。 (這只是一個簡短的描述,但這是廣泛的博客; 示例討論 。)

換句話說: async void方法的某些部分將async void運行的決定性因素不是它用await調用,而是在它的主體await它之后的一些有意義的工作。

(3b) async void另一個角度

由於基本q(出於Polly.Contrib.Decorator的目的)是我們應該如何調用包裝方法,因此我們可以圍繞async void方法運行替代性思維實驗。 如果我們可以(以某種方式)確定接口后面的void方法已被聲明為async void怎么辦? 我們會以不同的方式稱呼它嗎?

回到我們的示例, async void Foo() 我們有什么選擇? 我們可以直接Foo()Task.Run(() => Foo())

正如Stephen Cleary所介紹的, 庫不應該代表調用者使用Task.Run() 這樣做意味着庫正在選擇將工作卸載到后台線程上,從而拒絕調用方的選擇。 注意 :根據上述有關async void方法如何操作的討論,這僅適用於反正被調用方法中第一次await的工作。)

所以:即使我們能知道背后的方法void Foo()的接口是async void Foo() Polly.Contrib.Decorator仍然應該只調用Foo()反正。 如果用戶希望立即將工作卸載到另一個線程上(例如,他們希望將其卸載到GUI線程上),則他們(在引入Polly.Contrib.Decorator之前)將使用Task.Run(() => ...)調用該接口方法Task.Run(() => ...)反正。 我們不需要添加額外的內容。

這符合Polly遵循的原則,我建議遵循其他影響委托的庫:它對用戶委托的運行方式的影響最小(除了聲明的預期影響)。


以上所有內容的關鍵是async關鍵字( 不會本身) 不會使方法異步甚至並發運行 ,因此問題就不會解決。 async關鍵字僅允許編譯器在await語句中將方法分成一系列塊; 這種方法的塊2..n在先前的await調用完成后作為繼續運行(異步;在不同的時間)。 調用者(除async void方法外)返回了一個Task ,它是一個“承諾”,將在await方法完成時完成。

Task (或其他實現GetAwaiter()東西GetAwaiter()被返回的事實是是否可以通過await調用它的決定因素:如果方法的返回類型實現了此等待模式,則可以等待它。

異步/等待省略和特別是sync-cache-over-async-io模式的存在證明了被調用方法的返回類型是關鍵,而不是實現是否使用async關鍵字。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM