【问题标题】:Expose Action<T> as Action<object>将 Action<T> 公开为 Action<object>
【发布时间】:2011-09-30 11:21:22
【问题描述】:

我正在创建一个框架,其中包含一个库(特别是 SharpBrake)的包装器,它通过反射执行与 SharpBrake 的所有交互,因此我的框架的第 3 方对库没有硬性依赖。

如果我的框架的第 3 方想要使用 SharpBrake,他们可以将 SharpBrake.dll 塞入 bin 文件夹,但如果他们不这样做,他们可以忘记它。如果我的框架有对 SharpBrake 类型的显式引用,我的框架的用户会在缺少 SharpBrake.dll 的运行时遇到异常,这是我不希望的。

所以,我的包装器首先从磁盘加载 SharpBrake.dll,找到 AirbrakeClient 类型,并将指向 AirbrakeClient.Send(AirbrakeNotice) 方法的委托存储在私有字段中。然而,我的问题是,由于Send() 方法采用AirbrakeNotice 对象并且我不能直接引用AirbrakeNotice 对象,我需要以某种方式将Send() 方法转换为Action&lt;object&gt;

我有一种强烈的感觉,这是不可能的,但我想在决定公开Delegate 和使用DynamicInvoke() 之前探索所有选项,我认为这在性能方面远非最佳。我想做的是:

Type clientType = exportedTypes.FirstOrDefault(type => type.Name == "AirbrakeClient");
Type noticeType = exportedTypes.FirstOrDefault(type => type.Name == "AirbrakeNotice");
MethodInfo sendMethod = clientType.GetMethod("Send", new[] { noticeType });
object client = Activator.CreateInstance(clientType);
Type actionType = Expression.GetActionType(noticeType);
Delegate sendMethodDelegate = Delegate.CreateDelegate(actionType, client, sendMethod);

// This fails with an InvalidCastException:
Action<object> sendAction = (Action<object>)sendMethodDelegate;

但是,这会失败并出现以下异常:

System.InvalidCastException:无法将“System.Action`1[SharpBrake.Serialization.AirbrakeNotice]”类型的对象转换为“System.Action`1[System.Object]”类型。

显然,因为sendMethodDelegateAction&lt;AirbrakeNotice&gt; 而不是Action&lt;object&gt;。由于我不能在我的代码中提及AirbrakeNotice,我不得不这样做:

Action<object> sendAction = x => sendMethodDelegate.DynamicInvoke(x);

或者直接暴露Delegate sendMethodDelegate。这可能吗?我知道有可能会遇到object 可能与AirbrakeNotice 不同类型的情况,这会很糟糕,但是看到无论如何你可以搞砸反射,我希望某处存在漏洞。

【问题讨论】:

  • 如果您担心性能,我会避免扩展使用反射。如果您可以为您正在使用的课程提供interface,那么我会创建一个。然后编写一个包装器,通过调用 SharpBreak 代码来实现接口,并将其填充到单独的 DLL 中。然后动态加载您的包装程序集和具体包装类型,并调用该接口。那么您就不必在方法级别进行反射了。
  • @Merlyn 我希望我能多次为你投票。
  • 在快速查看他们的代码之后,这可能就像包装 SharpBrake.Extensions 一样简单,它有两种方法。您是否需要在应用中调用更多的扩展方法?
  • @MerlynMorgan-Graham,请发布带有代码示例的答案。我不太明白如何使用接口来实现我想要的松散耦合。
  • @asbjornu:您仍然必须使用反射,但仅限于类型。我添加了一个带有代码示例的答案。很难从该库中准确说出您在 your 应用程序中需要什么。该库的文档只是说要调用单个扩展方法,而您似乎正在尝试包装所有库。

标签: c# generics reflection delegates


【解决方案1】:

如果您乐于使用表达式树,这相当简单:

ConstantExpression target = Expression.Constant(client, clientType);

ParameterExpression parameter = Expression.Parameter(typeof(object), "x");
Expression converted = Expression.Convert(parameter, noticeType);
Expression call = Expression.Call(target, sendMethod, converted);

Action<object> action = Expression.Lambda<Action<object>>(call, parameter)
                                  .Compile();

认为这就是你想要的......

【讨论】:

  • 这确实是我所要求的。感谢您的精彩回答!但是,在这里阅读其他一些建议时,我可能会要求错误的东西,因此我将在接受任何答案之前测试提供的不同解决方案。
  • 自己经常做大量的反射实现,我写了a helper class which does this。例如。 DelegateHelper.CreateDelegate&lt;Action&lt;object&gt;&gt;( sendMethod, target, CreateOptions.Downcasting )。关于它的一些信息can be found here.
【解决方案2】:

如果您不需要低于 C# 4 的支持,您可以使用 dynamicDynamicInvoke 获得更好的性能。

Action<dynamic> sendAction = x => sendMethodDelegate(x);

实际上,如果您可以使用动态,我猜您甚至不需要上述内容,因为如果您这样做,它会提高性能并简化一切:

Type clientType = exportedTypes.FirstOrDefault(type => type.Name == "AirbrakeClient");
dynamic client = Activator.CreateInstance(clientType);

...
client.Send(anAirbrakeNotice);

但是如果你需要支持 .net 3.5 jon skeets 用表达式树回答肯定是要走的路。

【讨论】:

  • 这对于 .NET 4.0 来说是个好主意。我没有考虑过这种可能性,但为此需要 .NET 4 实际上可能是有意义的。我会让它酝酿一段时间,谢谢你的建议!
【解决方案3】:

来自我对 OP 的评论:

如果您担心性能,我会避免扩展使用反射。如果您可以为您正在使用的类提供一个接口,那么我会创建一个。然后编写一个包装器,通过调用 SharpBreak 代码来实现接口,并将其填充到单独的 DLL 中。然后仅动态加载您的包装程序集和具体包装器类型,并调用该接口。这样你就不必在方法级别进行反射了。

我不确定您需要的所有类,但这里有一个简单的示例,说明如何通过基于接口的松散耦合连接到该库。

在程序的程序集中:

public IExtensions
{
    void SendToAirbrake(Exception exception);
}

public static AirbreakExtensions
{
    private static IExtensions _impl;

    static()
    {
        impl = new NullExtensions();
        // Todo: Load if available here
    }

    public static void SendToAirbrake(this Exception exception)
    {
        _impl.SendToAirbrake(exception);
    }
}

internal class NullExtensions : IExtensions // no-op fake
{
    void SendToAirbrake(Exception exception)
    {
    }
}

在加载时可用(通过反射)程序集

public ExtensionsAdapter : IExtensions
{
    void SendToAirbrake(Exception exception)
    {
        SharpBrake.Extensions.SendToAirbrake(exception);
    }
}

这种方法的优点是您只使用一次反射(在加载时),并且再也不会触摸它。修改为使用依赖注入或模拟对象(用于测试)也很简单。

编辑:

对于其他类型,这需要更多的工作。

您可能需要use the Abstract Factory pattern 来实例化AirbrakeNoticeBuilder,因为您需要直接处理接口,并且不能将构造函数放在接口中。

public interface IAirbrakeNoticeBuilderFactory
{
    IAirbrakeNoticeBuilder Create();
    IAirbrakeNoticeBuilder Create(AirbrakeConfiguration configuration);
}

如果您要处理自定义 Airbreak 结构,您将需要做更多的工作。

例如对于AirbrakeNoticeBuilder,您必须为您使用的任何相关类创建重复的 POCO 类型。

public interface IAirbrakeNoticeBuilder
{
    AirbrakeNotice Notice(Exception exception);
}

由于您要返回 AirbrakeNotice,您可能必须拉入 Serialization 文件夹下的几乎所有 POCO,具体取决于您使用的数量以及传递回框架的数量。

如果您决定复制 POCO 代码,包括整个对象树,您可以look into using AutoMapper to convert to and from your POCO copies

或者,如果您不使用要返回的类中的值,而只是将它们传递回 SharpBreak 代码,您可以提出某种不透明的参考方案,该方案将使用您的字典实际 POCO 类型的不透明引用类型。然后你不必将整个 POCO 对象树复制到你的代码中,也不需要花费太多的运行时开销来来回映射对象树:

public class AirbrakeNotice
{
    // Note there is no implementation
}

internal class AirbreakNoticeMap
{
    static AirbreakNoticeMap()
    {
        Map = new Dictionary<AirbreakNotice, SharpBreak.AirbreakNotice>();
    }

    public static Dictionary<AirbreakNotice, SharpBreak.AirbreakNotice> Map { get; }
}

public interface IAirbrakeClient
{
    void Send(AirbrakeNotice notice);
    // ...
}

internal class AirbrakeClientWrapper : IAirbrakeClient
{
    private AirbrakeClient _airbrakeClient;

    public void Send(AirbrakeNotice notice)
    {
        SharpBreak.AirbrakeNotice actualNotice = AirbreakNoticeMap.Map[notice];
        _airbrakeClient.Send(actualNotice);
    }

    // ...
}

internal class AirbrakeNoticeBuilderWrapper : IAirbrakeNoticeBuilder
{
    AirbrakeNoticeBuilder _airbrakeNoticeBuilder;

    public AirbrakeNotice Notice(Exception exception)
    {
        SharpBreak.AirbrakeNotice actualNotice =
            _airbrakeNoticeBuilder.Notice(exception);

        AirbrakeNotice result = new AirbrakeNotice();
        AirbreakNoticeMap.Map[result] = actualNotice;

        return result;
    }

    // ...
}

请记住,您只需要包装您将要使用的公共接口的类和部分。即使您不包装其整个公共接口,该对象仍将在内部表现相同。这可能意味着您必须做更少的工作,因此请努力思考并尝试仅包装您现在需要的东西,以及您知道将来会需要的东西。记住YAGNI

【讨论】:

  • 这个特殊的例子有点奇怪,因为你用一个非静态类包装了一个静态类。它的工作原理,寿。如果您有其他要包装的类(尤其是实例类),请给我一个示例,我会给您另一个代码示例。
  • 所以您想在我的应用程序和 SharpBrake.dll 之间添加一个程序集,该程序集对 SharpBrake 有显式依赖,并且是延迟加载的(而不是 SharpBrake.dll)?实际上,这不是一个可怕的想法。甚至可以使用 IoC 来进行注册,而不仅仅是在 bin 文件夹中包含 .dll。
  • @asbjornu:是的,完全正确。对 IoC 表示肯定,而不是显式反射加载 :)
【解决方案4】:

对于此类问题,我真正喜欢的编程风格是编写尽可能多的强类型代码,然后将逻辑从动态类型代码移交给强类型代码。所以我会这样写你的代码:

  //your code which gets types
  Type clientType = exportedTypes.FirstOrDefault(type => type.Name == "AirbrakeClient");
  Type noticeType = exportedTypes.FirstOrDefault(type => type.Name == "AirbrakeNotice");

  //construct my helper object
  var makeDelegateHelperType=typeof(MakeDelegateHelper<,>).MakeGenericType(clientType, noticeType);
  var makeDelegateHelper=(MakeDelegateHelper)Activator.CreateInstance(makeDelegateHelperType);

  //now I am in strongly-typed world again
  var sendAction=makeDelegateHelper.MakeSendAction();

这是帮助器对象的定义,它能够通过更少的反射调用而摆脱困境。

public abstract class MakeDelegateHelper {
  public abstract Action<object> MakeSendAction();
}

public class MakeDelegateHelper<TClient,TNotice> : MakeDelegateHelper where TClient : new() {
  public override Action<object> MakeSendAction() {
    var sendMethod = typeof(TClient).GetMethod("Send", new[] { typeof(TNotice) });

    var client=new TClient();
    var action=(Action<TNotice>)Delegate.CreateDelegate(typeof(Action<TNotice>), client, sendMethod);
    return o => action((TNotice)o);
  }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-09-09
    • 2018-04-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多