65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 4.0 动态对象与 MEF 趣味实践 - 动态文件系统包装器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.52/5 (9投票s)

2009 年 9 月 4 日

CPOL

11分钟阅读

viewsIcon

83975

downloadIcon

610

探索我们可以在 .NET 4.0 和 C# 中利用 System.Dynamic 命名空间中的 DynamicObject 和 MEF 实现的令人兴奋的功能。

引言

本文主要介绍通过初步探索以下内容来做一些有趣的事情:

  • 在 .NET 4.0 中引入的 System.Dynamic 命名空间
  • System.CompositionModel 命名空间(托管可扩展性框架或 MEF - http://mef.codeplex.com/

简要说明 C# 4.0 的动态特性

C# 4.0 引入了 dynamic 关键字来支持动态类型。如果您将一个对象赋值给一个动态类型变量(例如 dynamic myvar=new MyObj()),那么对 myvar 的所有方法调用、属性访问和运算符调用都将被推迟到运行时,编译器在编译时不会对 myvar 进行任何类型检查。因此,如果您执行类似 myvar.SomethingStupid(); 的操作,它在编译时是合法的,但在运行时如果赋值给 myvar 的对象没有 SomethingStupid() 方法,则会无效。

System.Dynamic 命名空间包含各种支持动态编程的类,主要是 DynamicObject 类,您可以从中派生自己的类来自行处理运行时分派。

您可能还想进一步了解鸭子类型,并阅读关于 dynamic 关键字DLR(动态语言运行时)的一些内容。

回到本文。

本文的关键目标是演示 C# 和 .NET 4.0 的动态功能所带来的多维度可能性和令人兴奋的体验。我写这篇文章的目的是将其作为实现 C# 动态功能的一个学习材料,并且随附的代码仅供参考其实现技术,而不是它实际的功能

我们将有几个有趣的目标:

  • 创建一个文件系统的动态包装器,以便我们可以将文件和目录作为动态对象的属性/成员来访问。
  • 一种将自定义方法和运算符附加到我们的动态包装器类并将其分派到插件子系统的方法。

我写这篇文章的目的是将其作为实现 C# 动态功能的一个学习材料,并且附带的代码仅供参考其实现技术。因此,这些目标是从学习的角度出发的,通过动态类型访问文件系统可能没有显著的优势。

您可能需要安装 VS2010 和 .NET 4.0 Beta 才能查看代码。 点击此处

我们将实现什么

简单来说,在一天结束时,我们将能够做以下几件事:

  • 初始化一个动态包装的驱动器
  • dynamic CDrive = new FileSystemStorageObject(@"c:\\");
  • 创建一个名为 TestSub 的子目录
  • CDrive.CreateSubdirectory("TestSub");
  • 神奇之处 - 在刚刚创建的 TestSub 文件夹中创建一个名为 File1.txt 的文件
  • using (var writer = CDrive.TestSub.File1.txt.CreateText())
    {
        writer.WriteLine("some text in file1.txt");
    }
  • 神奇之处 - 使用 Get/Set 方法包装属性。例如,调用 CreationTime 属性。
  • Console.WriteLine(CDrive.TestSub.File1.txt.GetCreationTime());
  • 更多神奇之处 - 使用 >> 运算符将 File1.txt 复制到 File2.txt
  • var result = (CDrive.TestSub.File1.txt >> CDrive.TestSub.File2.txt);
  • 更多神奇之处 - 另一种复制方式,但通过我们的动态类型作为参数调用 FileInfo 中的方法
  • CDrive.TestSub.File2.txt.CopyTo(CDrive.TestSub.File3.txt); 
  • 删除新创建的文件夹
  • CDrive.TestSub.Delete(true);

高层视图

以下是涉及的类和接口的快速高层视图:

image001.jpg

  • DynamicStorageObject – 继承自 System.Dynamic.DynamicObject 类。封装了使用 MEF 加载插件的逻辑,并调用特定类型的方法或属性。
  • FileSystemStorageObject - DynamicStorageObject 的具体实现,用于文件系统。
  • CommandLoader – 依赖 MEF 通过检查元数据来加载特定类型的的方法和运算符扩展。
  • IDynamicStorageCommand – 所有方法和运算符插件都应实现的接口。

此外,以下是我们将在继承类中重写的 System.Dynamic.DynamicObject 类中的主要方法:

  • TrySetMember - 提供设置成员的实现。
  • TryGetMember - 提供获取成员的实现。
  • TryInvokeMember - 提供调用成员的实现。
  • GetDynamicMemberNames - 返回所有动态成员名称的枚举。
  • TryBinaryOperation - 提供执行二进制运算的实现。

现在,来看代码。

将子文件夹和文件公开为属性

在上面的示例中,您可能已经注意到我们将子目录和文件公开为动态对象的属性,例如 CDrive.TestSub.File2.txt。并且我们也支持扩展。

让我们快速了解一下这是如何处理的。首先,我们通过重写 GetDynamicMemberNames(请参阅 FileSystemStorageObject.cs)返回所有动态成员名称。

public override IEnumerable<string> GetDynamicMemberNames()
{
    return Directory.GetFileSystemEntries(CurrentPath, Filter).AsEnumerable();
}

现在,由于 CDriveFileSystemStorageObject 类型的动态对象,每当访问一个属性时,运行时都会调用 FileSystemStorageObject 中的 TryGetMember 方法。

查看 FileSystemStorageObject 类中的 TryGetMember 方法。如前所述,每当访问动态对象的属性时,都会调用此方法。我们的 TryGetMember 实现有点棘手,因为我们需要决定返回的对象是代表有效的文件或文件夹(路径),还是仅仅是一个虚拟对象(用于处理像 File2.txt 中的 txt 这样的扩展名)。

public override bool TryGetMember(GetMemberBinder binder, out object result)
{
    var path = Path.Combine(CurrentPath, binder.Name);

    if (TryGetQualifiedPath(binder.Name, path, out result))
        return true;
    else
    {
        result = new FileSystemStorageObject(CurrentPath, binder.Name, 
                                             Filter, this, true);
        return true;
    }
}

TryQualifiedPath 方法用于返回一个带有合格路径的新 FileSystemStorageObject 实例;否则,如果它是一个扩展名,我们将返回一个虚拟的 FileSystemStorageObject,并将虚拟参数设置为 true。如果您更好奇,可能需要在此处设置几个断点并进行一些调试。

分派方法调用

每当在 FileSystemStorageObject 的动态实例上发生方法调用时,例如 CDrive.TestSub.File2.txt.CopyTo(..),在后台,运行时都会调用 TryInvokeMember。查看我们在 FileSystemStorageObject 类中实现的 TryInvokeMember 方法。

public override bool TryInvokeMember(InvokeMemberBinder binder, 
                     object[] args, out object result)
{

    if (Commands.ContainsKey(binder.Name))
    {
        //Execute a custom command
        Commands[binder.Name].Execute(this,null, args, out result);
        return true;
    }          
    else if (Directory.Exists(CurrentPath) && StoreItemType!=ObjectType.Unspecified )
    {
        DirectoryInfo info = new DirectoryInfo(CurrentPath);
        if (TryInvokeMethodOrProperty(info, binder.Name, args, out result))
            return true;
    }
    else
    {
        //Treat as a file command
        FileInfo info = new FileInfo(ProjectedPath);
        if (TryInvokeMethodOrProperty(info, binder.Name, args, out result))
            return true;
    }

    throw new InvalidOperationException
        (string.Format(Properties.Settings.Default.ErrorInvalid, 
                       binder.Name, MemberName));
}

尽管代码本身易于理解,但此处对我们如何处理动态对象上的方法调用做一点进一步的解释。

  • 如果方法名称已存在于任何注册的插件中,则该插件将被调用。您可以看到 Commands 属性包含一个预加载插件的集合。
  • 否则,如果路径是一个目录,我们尝试在相关的 DirectoryInfo 对象上调用方法。
  • 否则(如果当前路径是文件,或者当前路径不存在,我们就会到达这里),我们将当前路径视为一个文件,并尝试为相关的 FileInfo 对象调用方法。

第三步使得我们能够调用不存在路径上的方法,例如:

using (var writer = CDrive.TestSub.File1.txt.CreateText())
{
    writer.WriteLine("some text in file1.txt");
}

另一个有趣的地方可能是由基类 DynamicStorageObject 定义的 TryInvokeMethodOrProperty 方法。如果您有 VS 2010 的钻头(driller),可以深入代码快速查看。

正如您可能想到的,此方法的一个明显任务是在指定对象上调用给定的方法 – 但更重要的是,它还允许您将 FileInfoDirectoryInfo 的属性作为 Get/Set 方法调用来调用,例如 CDrive.TestSub.GetCreationTime() 来访问 CreationTime 属性。这是因为,根据我们到目前为止的约定,CDrive.TestSub.CreationTime 代表 TestSub 中名为 CreationTime 的文件夹或文件,而不是 TestSub 的属性。

使用 MEF - 插入方法

托管可扩展性框架或 MEF - http://mef.codeplex.com/,或者那些无聊的人称之为 System.CompositionModel 命名空间,是热门又酷的东西。嗯,在撰写本文时,它仍处于 Beta 预览阶段。

我们在这里使用 MEF 来支持为我们的动态对象插入方法调用和运算符。在这里,我基本上是根据我们的应用程序的上下文来解释 MEF 的用法;您可以通过访问上面的 URL 来了解更多信息。

以下是使用 MEF 创建插件子系统的最小方法:

创建插件实现的契约

首先,我们为我们的插件子系统创建一个契约。请查看 DynamicFun.Lib 项目中的 IDynamicStorageCommand 接口。

创建和“导出”插件

创建几个实现此接口的插件,并使用 Export 属性“导出”它们,将契约作为参数提供,以便我们稍后可以发现它们。例如,请查看 DynamicFun.Commands 项目中的 BackupOperation 实现。

[Export(typeof(IDynamicStorageCommand))]
[ExportMetadata("Command", "Backup")]
[ExportMetadata("Type", typeof(FileSystemStorageObject))]
public class BackupOperation : IDynamicStorageCommand
{
    #region IDynamicStorageCommand Members

    public bool Execute(DynamicStorageObject caller, 
                object partner, object[] args, out object result)
    {
        result = null;
        var path = caller.CurrentPath;

        if (File.Exists(path) && !path.EndsWith(".backup", 
                              StringComparison.InvariantCultureIgnoreCase))
        {
            File.Copy(path, path + ".backup",true);
            return true;
        }
      
        return false;
    }

    #endregion
}

您可能会注意到我们也导出了元数据。稍后我们将讨论这一点。

“导入”插件

显然,我们需要导入这些插件以便稍后使用它们。我们有 CommandLoader 类来完成这项工作。请查看 CommandLoader 类的构造函数。

[ImportMany]
private Lazy<IDynamicStorageCommand, IDictionary<string, 
                object>>[] loadedCommands { get; set; }

public CommandLoader(Type type,string filter) 
{                
    var catalog = new DirectoryCatalog(System.Environment.CurrentDirectory, filter);
    var container = new CompositionContainer(catalog);
    container.ComposeParts(this);
    _compatibleType = type;
}

ImportMany 属性告诉 MEF,当此部分被组合(解析)时,MEF 应该从目录(在此例中,我们使用目录目录从程序集中加载插件)将所有导出的类型实例导入到此集合中。在后台,MEF 将初始化集合,并创建导出的类型的实例以将它们添加到集合中。

同时,请查看我们如何初始化 CommandLoader。我们在 DynamicStorageObject 类的构造函数中创建 CommandLoader 的实例。

if (CommandsCache == null)
    CommandsCache = new Dictionary<Type, Dictionary<object, IDynamicStorageCommand>>();

if (!CommandsCache.ContainsKey(this.GetType()))
{
    var loader = new CommandLoader(this.GetType(), "*.Commands.*");
    CommandsCache.Add(this.GetType(), loader.Commands);
}

加载器的 Commands 属性将返回与当前类型兼容的命令的字典。键值对的键将是我们作为插件元数据导出的“Command”参数的值(例如,“Backup”)。值将是实际的 IDynamicStorageCommand 类型的插件本身。

类型兼容性检查,以确定此命令是否与调用者兼容,是通过我们作为插件元数据导出的“Type”参数的值来完成的。

将方法调用分派到插件

现在,当用户调用方法如 CDrive.TestSub.File1.Txt.Backup() 时,如前所述,运行时会调用 TryInvokeMember。如果方法名称存在于 Commands 字典中,则将方法调用分派到插件非常直接。请查看 TryInvokeMember 方法,我们从那里调用命令的 Execute 方法。

if (Commands.ContainsKey(binder.Name))
{
    //Execute a custom command
    Commands[binder.Name].Execute(this,null, args, out result);
    return true;
}

分派运算符

最后,我们将研究当运算符应用于我们的动态对象时,我们如何分派它们。您可能已经看到我们如何使用右移运算符来执行复制操作,例如:

var result = (CDrive.TestSub.File1.txt >> CDrive.TestSub.File2.txt);

将运算符调用分派到插件子系统与我们为方法执行的操作非常相似。但是,需要注意的关键点是,我们是从 DynamicStorageObject 类中的 TryBinaryOperation 方法执行此操作的。此外,在导出元数据时,我们应该将运算符指定为“Command”参数的值。例如,请查看 CopyOperation

[Export(typeof(IDynamicStorageCommand))]
[ExportMetadata("Command", System.Linq.Expressions.ExpressionType.RightShift)]
[ExportMetadata("Type", typeof(FileSystemStorageObject))]
public class CopyOperation : IDynamicStorageCommand
{
   //..
}

结论

本文只是为了介绍一些新的框架功能,目的是向好奇的读者演示它们。因此,在实现中,您可能会注意到我们有各种功能限制,例如不支持带空格的文件或文件夹等。

几点说明

  1. 动态调用在第一次调用时会较慢;解析后的调用站点将被 JIT 编译并尽可能缓存以供后续所有调用使用。
  2. C# 的底层类型系统在 4.0 中没有改变。只要您不使用 dynamic 关键字,您仍然是静态类型的(即,编译器在编译时就知道类型)。
  3. 使用动态特性时的错误处理有点困难,而且可能不够具体,因为您对分派调用的外部对象了解不多。

此外,在这篇文章中,我没有讨论可以实现动态分派的场景。但是,一些有趣的可能性包括使用 C# 操作 HTML DOM,拥有一个流畅的包装器来访问 XML 数据岛等等:)

在未来的帖子中,我可能会就这些主题写更多内容。目前就这些。

附录 - I

当我开始收到很多“害怕”(见下文)的评论时,我想添加这个附录:)

我想重申,我写这篇文章的目的是将其作为解释 C# 中可用动态特性的学习材料,并且开发者可以自行决定**何时**使用它。

我在这里列出一些“C# 4.0 新功能”手册中的要点,关于您可能越来越多地使用它的场景 - [获取完整文档][^]

“C# 4.0 的主要主题是动态编程。对象越来越“动态”,意味着它们的结构和行为不被静态类型捕获,或者至少不是编译器在编译您的程序时所知道的。一些例子包括:

  • 来自 Python 或 Ruby 等动态编程语言的对象
  • 通过 IDispatch 访问的 COM 对象
  • 通过反射访问的普通 .NET 类型
  • 结构不断变化的对象的,例如 HTML DOM 对象

附录 - II

我有一个第二部分,但由于它对于 CodeProject 文章来说太短了,我决定将其保留为一篇博客文章。您也可以阅读这篇文章。阅读 MyExpando 类 - System.Dynamic.ExpandoObject 的最小实现

历史

  • 2009 年 9 月 4 日,星期五 - 发布。
© . All rights reserved.