R语言S4对象序列化到.NET对象
我想要开发一个简单的包装器操作来自动完成混合编程数据转换的工作。这让我的实验室科学研究工作变得愉快!
下载链接
Shoal Shell语言的全部源代码可以从SourceForge SVN服务器下载
svn checkout svn://svn.code.sf.net/p/shoal/Source/ shoal-Source
本文中的测试示例源代码
关于Shoal Shell和混合编程的相关链接
引言
在读取R表达式的计算数据时,在VB和R语言之间进行混合脚本编写非常痛苦,因此我想开发一个简单的包装器操作来自动完成这项数据转换工作。
在我最近的实验室科学研究工作中,我想要分析虚拟细胞实时基因芯片数据中的基因表达调控信号。而R版本的小波库可以完美地完成这项工作,所以本文中的代码使得这种混合编程变得愉快而简单。
Using the Code
引用混合编程的步骤概述1. 在.NET类对象属性和S4Object属性之间创建映射
2. R表达式求值
3. 将R符号表达式序列化为.NET对象实例。
所以,就是这样,只需3个简单的步骤,您就可以在VB/C#和R语言之间进行混合编程。让我们一步一步来学习。
1. 在.NET类对象属性和S4Object属性之间创建映射
这一步是创建R对象和你的.NET对象之间的模式映射,与XML序列化类似,在创建XML文档使用XML序列化之前,您应该定义一个类对象来描述文档的XML格式;在类型定义之后,您就可以创建一个XML文档了。
所以,在这一步的操作与XML序列化中的操作相同,但XML序列化与此R对象序列化之间的区别在于,我们只是使用了不同的自定义属性。
在创建映射之前,让我们先学习R语言中的类型
在我看来,R对象可以分为3种类型
S4Object
,s4object
就像.NET语言中的类对象。 .NET对象中的属性等于R语言中的s4object
属性(或槽)。本文代码的主要功能是在我们的.NET类对象和R的s4object
之间实现映射。Function
,R语言中的函数对象就像.NET语言中的lambda表达式或委托,R中函数的声明就像.NET中lambda表达式的声明。- Generic vector,泛型向量是R语言中最常用的对象,因为R语言几乎所有对象都是向量。就像.NET中的数组或列表一样,向量可以是R语言中
s4object
的属性(或属性),也可以由s4object
集合组成。
所以,您可以看到在.NET语言中,我们的类对象等同于R语言中的s4object
,因此我们在这些步骤中创建的映射是针对class
属性的。s4object
属性与.NET类属性之间的映射使用了DataFrameColumnAttribute
,它位于Microsoft.VisualBasic.ComponentModel.DataSourceModel
命名空间中,从客户属性DataFrameColumnAttribute
的类定义中可以看出,此属性只能应用于属性或字段。
Namespace ComponentModel.DataSourceModel
''' <summary>
''' Represents a column of certain data frames. The mapping between to schema
''' is also can be represent by this attribute.
''' (也可以使用这个对象来完成在两个数据源之间的属性的映射,由于对于一些列名称的属性值缺失的映射而言,
''' 其是使用属性名来作为列映射名称的,故而在修改这些没有预设的列名称的映射属性的属性名的时候,请注意
''' 要小心维护这种映射关系)
''' </summary>
<AttributeUsage(AttributeTargets.[Property] Or AttributeTargets.Field, Inherited:=True,
AllowMultiple:=False)> _
Public Class DataFrameColumnAttribute : Inherits Attribute
下面是一个使用此属性创建映射的示例代码
Imports Microsoft.VisualBasic.ComponentModel.DataSourceModel
Public Class Filter
<DataFrameColumn> Public Property L As Integer
<DataFrameColumn("level")> Public Property level As Integer
<DataFrameColumn("h")> Public Property h As Double()
<DataFrameColumn("g")> Public Property g As Double()
<DataFrameColumn("wt.class")> Public Property wtclass As String
<DataFrameColumn("wt.name")> Public Property wtname As String
<DataFrameColumn("transform")> Public Property transform As String
<DataFrameColumn("class")> Public Property [class] As String
End Class
正如您所见,第一个属性
<DataFrameColumn> Public Property L As Integer
它们的映射没有列名,因此在创建映射时,序列化器将自动使用其属性名作为映射名。
映射需要一个name
属性,因为R s4object
中的某些属性在.NET语言中是非法的,例如.NET属性名中的wt.class
是不允许的,因此您可以使用DataFrameColumn
映射属性来完成此任务。
2. R表达式求值
我们将使用RDotNET
从R获取结果;这个库是我们实现VB/C# .NET语言与R语言混合编程的最完美解决方案。
您可以从codeplex主页下载RDotNET
库
.NET语言和R语言之间的混合编程只需两个简单的步骤
首先,启动R引擎服务,例如
If Not String.IsNullOrEmpty(R_HOME) Then
Wavelets.R = RDotNET.REngine.StartEngineServices(R_HOME)
Else
Wavelets.R = RDotNET.REngine.StartEngineServices
End If
Call Wavelets.R.Library(PackageName:="wavelets")
启动R引擎服务需要一个R_HOME
值,这是您安装R程序所在的目录,例如R安装程序的默认位置。
C:\Program Files\R\R-3.1.3\bin
如果您的R程序在计算机上正确安装,那么RDotNET
可以根据R程序的注册表值自动搜索R_HOME
,然后您就可以使用RDotNET.REngine.StartEngineServices
的无参数版本来创建实例。如果不行,那么您可以使用RDotNET.REngine.StartEngineServices(R_HOME)
手动设置R的安装位置。
使用RDotNET
创建R引擎服务实例后,您就可以在.NET程序中进行编码了。在混合编程中需要注意的是,R中的许多分析程序并非最初包含在基础包中,**因此在运行程序之前,您应该在R终端中安装所需的R包。** 完成并成功安装R包后,您就可以使用REngine
中的Library
函数来加载所需的库包。
Call Wavelets.R.Library(PackageName:="wavelets")
或者您也可以将此步骤放在脚本步骤中
Dim STDOUT = Wavelets.R <= "library(""wavelets"")"
然后,您只需简单地调用R计算,使用R.Evaluate
函数,该函数返回RDotNET
符号表达式对象,该对象将R内存暴露给您的.NET程序。与RDotNET
中的<=
运算符不同,<=
运算符返回在终端控制台上显示的STDOUT
字符串集合。
3. 将R符号表达式序列化为.NET对象实例
在这一步,我们只需一个语句就可以将RDotNET
符号表达式序列化为.NET对象,您的R语言混合编程将保持简单和愉快:-)
我们假设您已经在程序中正确创建了映射类对象,并且已经从R调用求值中获得了一个结果值,那么您就可以像下面所示的操作一样简单地进行序列化工作了。
Dim Result = RDotNET.Extensions.ShellScriptAPI.Serialization.LoadFromStream_
(Of Wavelets.Waveletmodwt)(TestResultRS4Object)
此代码如何工作?
此序列化操作可以在命名空间位置找到:RDotNET.Extensions.ShellScriptAPI.Serialization
。有两个接口可以调用此序列化
Imports RDotNET.SymbolicExpressionExtension
''' <summary>
''' Convert the R object into a .NET object from the specific type schema information.
''' (将R之中的对象内存数据转换为.NET之中指定的对象实体)
''' </summary>
''' <remarks></remarks>
Public Module Serialization
''' <summary>
''' Deserialize the R object into a specific .NET object.
''' <see cref="RDotNET.SymbolicExpression"></see> =====> <see cref="T"></see>
''' </summary>
''' <typeparam name="T"></typeparam>
''' <param name="RData"></param>
''' <returns></returns>
''' <remarks>
''' 反序列化的规则:
''' 1. S4对象里面的Slot为对象类型之中的属性
''' 2. 任何对象属性都会被表示为数组
''' </remarks>
Public Function LoadFromStream(Of T As Class)(RData As RDotNET.SymbolicExpression) As T
Dim value As Object = InternalLoadFromStream(RData, GetType(T))
Return DirectCast(value, T)
End Function
''' <summary>
''' Needs your manual type casting in your program.
''' </summary>
''' <param name="RData"></param>
''' <param name="Type"></param>
''' <returns></returns>
''' <remarks></remarks>
Public Function LoadRStream(RData As RDotNET.SymbolicExpression, Type As Type) As Object
Dim value As Object = InternalLoadFromStream(RData, Type)
Return value
End Function
由于R中的s4object
可能在其属性中包含向量,并且向量中的元素可能是s4object
类型,因此s4object
的序列化是一个递归操作。所以首先,我们从这个函数开始递归操作
''' <summary>
''' Load the R symbolic expression data recursivly start from here.
''' </summary>
''' <param name="RData"></param>
''' <param name="TypeInfo"></param>
''' <returns></returns>
''' <remarks></remarks>
Private Function InternalLoadFromStream(RData As RDotNET.SymbolicExpression, _
TypeInfo As System.Type) As Object
Select Case RData.Type
Case Internals.SymbolicExpressionType.S4
'Load the R symbolic expression data recursivly start from here.
Return InternalLoadS4Object(RData, TypeInfo)
Case Internals.SymbolicExpressionType.LogicalVector
Return RData.AsLogical.ToArray
Case Internals.SymbolicExpressionType.CharacterVector
Return RData.AsCharacter.ToArray
Case Internals.SymbolicExpressionType.IntegerVector
Return RData.AsInteger.ToArray
Case Internals.SymbolicExpressionType.NumericVector
Return RData.AsNumeric.ToArray
Case Internals.SymbolicExpressionType.List
Return InternalCreateMatrix(RData, TypeInfo)
Case Else
Throw New NotImplementedException
End Select
End Function
正如您在这个函数中可以看到的,如果r
对象是s4object
,那么程序将继续递归操作,否则,如果对象是基本类型,那么函数将从递归操作中退出并返回值。在此序列化中,我们只是读取.NET语言中的简单数据类型:Boolean
、String
、Integer
、Double
和Object()
,其他数据类型如R中的函数(.NET语言中的lambda表达式)在此函数中被跳过,因为我们不知道如何将这些数据保存到文件系统中。
然后,如果我们要映射的对象是R语言中的s4object
,我们将进入递归操作步骤
Case Internals.SymbolicExpressionType.S4
'Load the R symbolic expression data recursivly start from here.
Return InternalLoadS4Object(RData, TypeInfo)
''' <summary>
''' The recursive operation of the S4Object in R starts from here.
''' This recursive operation will stop when the property value is not a S4Object.
''' (这个可能是一个递归的过程,一直解析到各个属性的R类型不再是S4对象类型为止)
''' </summary>
''' <param name="RData"></param>
''' <returns></returns>
''' <remarks></remarks>
Private Function InternalLoadS4Object(RData As RDotNET.SymbolicExpression, _
TypeInfo As System.Type) As Object
Dim Mappings = Microsoft.VisualBasic.ComponentModel.DataSourceModel._
DataFrameColumnAttribute.LoadMapping(TypeInfo)
Dim obj As Object = Activator.CreateInstance(TypeInfo)
Call Console.WriteLine("[DEBUG] {0} ---> R.S4Object (""{1}"")", _
TypeInfo.FullName, String.Join("; ", RData.GetAttributeNames))
For Each Slot In Mappings
Dim RSlot As RDotNET.SymbolicExpression = RData.GetAttribute(Slot.Key.Name)
Dim value As Object = InternalLoadFromStream(RSlot, Slot.Value.PropertyType)
Call InternalValueMapping(value, Slot.Value, obj:=obj)
Next
Return obj
End Function
我们将在这一步先加载映射,使用
Dim Mappings = Microsoft.VisualBasic.ComponentModel.DataSourceModel._
DataFrameColumnAttribute.LoadMapping(TypeInfo)
然后,我们创建一个目标映射类型的对象实例来包含数据。
Dim obj As Object = Activator.CreateInstance(TypeInfo)
由于S4Object
中的属性等同于.NET类属性,当我们从我们.NET程序的目标类型的模式定义中加载映射时,我们就可以加载R表达式中的数据,具体针对我们类的每个属性。For
循环中的步骤包含了这些步骤
- 获取
S4Object
中的特定属性作为映射序列化数据源Dim RSlot As RDotNET.SymbolicExpression = RData.GetAttribute(Slot.Key.Name)
- 然后我们可以继续递归地反序列化R表达式
Dim value As Object = InternalLoadFromStream(RSlot, Slot.Value.PropertyType)
- 最后,我们得到.NET格式的值,这样我们就可以使用反射操作将值赋给属性了。
Call InternalValueMapping(value, Slot.Value, obj:=obj)
矩阵值不能直接通过反射赋值。
正如您在前面的步骤中看到的,我们从序列化映射中获得的值不是直接赋给特定属性,而是使用一个函数来完成这项工作,这是因为R中的矩阵对象被映射为(对象数组)的数组……所以我们从R中得到的矩阵,实际上是一个对象数组(因为对象数组等同于对象类型,或者.NET中的一切都等同于对象类型,因为.NET中的所有数据类型都继承自对象类型),所以R中的矩阵实际上是.NET程序认为它是一个对象数组,而不是特定类型的数组的数组,所以当我们直接赋值矩阵值时,程序就会崩溃!
最后,我们得到一个Object()
,其中该数组的元素类型是Double()
,而不是我们想要的类型:Double()()
矩阵,这将导致异常。所以我们使用了函数
''' <summary>
'''
''' </summary>
''' <param name="value"></param>
''' <param name="pInfo"></param>
''' <param name="obj">对象实例</param>
''' <returns></returns>
''' <remarks></remarks>
Private Function InternalValueMapping(value As Object, _
pInfo As System.Reflection.PropertyInfo, ByRef obj As Object) As Boolean
Dim pTypeInfo As System.Type = pInfo.PropertyType
If pTypeInfo.HasElementType Then
Call InternalMappingCollectionType(value, pInfo, obj, pTypeInfo)
Else
Call InternalRVectorToNETProperty_
(pTypeInfo:=value.GetType, value:=value, obj:=obj, pInfo:=pInfo)
End If
Return True
End Function
来帮助我们正确地将向量矩阵类型转换为适当的.NET数组类型。
由于几乎所有的R数据类型都是向量,当我们的.NET类中的属性是单个元素,如string
/integer
/double
而不是向量string()
/integer()
/double()
时,因此当.NET类属性的反射类型是单个元素时,我们只需将r数据转换为数组并获取第一个元素值,事情就会顺利进行。当我们的.NET类属性中的数据类型是数组时,我们只需将r转换后的值直接赋给它,事情也进行得很好!
使用此函数将对象数组转换为特定类型的矩阵
''' <summary>
''' Object() to T()()
''' </summary>
''' <param name="value"></param>
''' <param name="pInfo"></param>
''' <param name="obj"></param>
''' <param name="pTypeInfo"></param>
''' <remarks></remarks>
Private Sub InternalMappingCollectionType(value As Object, _
pInfo As System.Reflection.PropertyInfo, ByRef obj As Object, pTypeInfo As System.Type)
Dim EleTypeInfo As Type = pTypeInfo.GetElementType
Dim SourceList = (From val As Object In DirectCast(value, System.Collections.IEnumerable) _
Select val).ToArray
Dim List = Array.CreateInstance(EleTypeInfo, SourceList.Count)
For i As Integer = 0 To SourceList.Count - 1
Call List.SetValue(SourceList(i), i)
Next
Call pInfo.SetValue(obj, List)
End Sub
我们可以在此反射操作函数中使用Array.CreateInstance
来创建特定类型的数组,在创建数组之前,我们应该知道它的元素类型,元素类型可以从属性类型的反射中得知。
Dim EleTypeInfo As Type = pTypeInfo.GetElementType
由于我们已经知道R转换后的数据是矩阵,我们直接将其转换为数组数据
Dim SourceList = (From val As Object In DirectCast_
(value, System.Collections.IEnumerable) Select val).ToArray
最后,我们知道了创建数组的两个关键要素:**其元素类型和数组中的元素数量(或者说数组大小)**
Dim List = Array.CreateInstance(EleTypeInfo, SourceList.Count)
在我们使用List.SetValue
将元素值赋给数组中的每个位置后,我们就在.NET程序中得到了一个数组(的数组)类型的矩阵。最后,我们可以将这个转换后的矩阵值赋给特定的属性。
Call pInfo.SetValue(obj, List)
一个简单的代码测试示例
在测试项目中,您可以学习如何轻松愉快地进行这种混合编程。测试项目中有两个模块
引用
Wavelets
模块,用于定义所需的r函数和r对象映射类型,以读取r调用返回的小波计算结果
Program
模块,用于测试示例代码
重要提示
在运行此代码之前,您的计算机上应正确安装R程序,并且R系统上应安装所需的**wavelets** R库。
1. 最简单的VB/C#混合编程示例
' VB/C# with R language hybrid programming example
Dim ChipData = (From row As Microsoft.VisualBasic.DataVisualization.DocumentFormat.Csv.File.RowObject
In Microsoft.VisualBasic.DataVisualization.DocumentFormat.Csv.File.FastLoad_
("../DM_1184.GeneChipDataSamples.csv")
Select ID = row.First, ExpressionData0 = (From s As String In row.Skip(1) _
Select Val(s)).ToArray).ToArray
Call Wavelets.Initialize()
Dim TestResultRS4Object = Wavelets.DWT_RInvoke(ChipData.First.ExpressionData0, filter:="haar")
Dim Result = RDotNET.Extensions.ShellScriptAPI.Serialization.LoadFromStream_
(Of Wavelets.Waveletmodwt)(TestResultRS4Object)
Call Result.GetXml.SaveTo("./Test.Result.xml")
程序代码遵循R混合编程的典型步骤
- 在函数中初始化R引擎服务并加载所需的库
Call Wavelets.Initialize()
- 然后调用R函数获取
RDotNET
符号表达式Dim TestResultRS4Object = Wavelets.DWT_RInvoke(ChipData.First.ExpressionData0, filter:="haar")
- 最后,我们通过序列化以.NET类格式获取结果
Dim Result = RDotNET.Extensions.ShellScriptAPI.Serialization.LoadFromStream_ (Of Wavelets.Waveletmodwt)(TestResultRS4Object)
调用小波信号分析只需要简单的3个编码步骤,对吗?:)
2. 与ShoalShell语言的混合脚本
Shoal Shell语言是一种新的嵌入式脚本语言,用于.NET,最初是为我的虚拟细胞系统开发的。**它具有与R/Perl/SQL/LINQ进行大量混合脚本编写的能力**,目前,我刚刚发布了针对shoal shell的R混合脚本API。
该示例展示了如何进行shoal/R与您的.NET程序的混合脚本编写
'Shoal Shell Script programming example
Dim ShoalShell As Microsoft.VisualBasic.Scripting.ShoalShell.Runtime.Objects.ShellScript = _
New Scripting.ShoalShell.Runtime.Objects.ShellScript()
Call ShoalShell.InstallModules(GetType_
(RDotNET.Extensions.ShellScriptAPI.Serialization).Assembly.Location)
Call ShoalShell.InstallModules(GetType(Wavelets).Assembly.Location)
Call ShoalShell.InstallModules(GetType_
(ShoalShell.PlugIns.Plot_Devices.DataSource).Assembly.Location)
Call ShoalShell.TypeLibraryRegistry.Save()
Dim Script As String =
<ShoalShell-Script>
imports wavelets
imports r.net
imports io_device.csv
imports system
chipdata < (imports.csv) ../DM_1184.GeneChipDataSamples.csv
chipdata <- $chipdata -> as.datasource
chipdata <= $chipdata [0]
chipdata <- $chipdata -> get.X
s4obj <- $chipdata -> dwt.r.invoke filter haar n.levels 5
result.type <- wavelets result.type.schema
result <- ctype r.data $s4obj cast.type $result.type
call $result > ./Test.Result.ShoalInvoke.xml
return $result
</ShoalShell-Script>
Dim bResult = ShoalShell <= Script 'Execute the script and gets the return value
MsgBox(DirectCast(bResult, Wavelets.Waveletmodwt).GetXml, MsgBoxStyle.Information)
首先,我们在代码中实例化一个shoal shell脚本主机,然后安装所需的模块DLL文件
Dim ShoalShell As Microsoft.VisualBasic.Scripting.ShoalShell.Runtime.Objects.ShellScript = _
New Scripting.ShoalShell.Runtime.Objects.ShellScript()
安装外部动态API模块DLL文件,您可以使用
Call ShoalShell.InstallModules("<DLL_filepath>")
例如
Call ShoalShell.InstallModules(GetType_
(RDotNET.Extensions.ShellScriptAPI.Serialization).Assembly.Location)
然后我们开始脚本编写并获取返回结果
# Shoal shell statement
return $result
' VB code gets the result from the shoal shell returns value
Dim bResult = ShoalShell <= Script 'Execute the script and gets the return value
3. 使用Shoal Shell进行动态编程
shoal shell还具有与您的.NET程序的动态编程功能
' Shoal Shell VB/C# dynamics programming example
Dim Dynamics As Object = New Microsoft.VisualBasic.Scripting.ShoalShell.Runtime.Objects.Dynamics_
(ShoalShell)
' ---------------------------Translate version of the shell shell scripting show above--------------
Dim ChipDataDy = Dynamics.Imports.Csv("../DM_1184.GeneChipDataSamples.csv")
ChipDataDy = Dynamics.As.DataSource(ChipDataDy)
ChipDataDy = ChipDataDy(0)
ChipDataDy = Dynamics.Get.X(ChipDataDy)
Dim s4obj = Dynamics.dwt.r.invoke(ChipDataDy)
Result = DirectCast(Dynamics.CType(s4obj, GetType(Wavelets.Waveletmodwt)), Wavelets.Waveletmodwt)
' ---------------------------------------------------------------------------------------------------
Result.GetXml.SaveTo("./Test.Result.ShoalInvoke.Dynamics.Programming.xml")
MsgBox(DirectCast(Result, Wavelets.Waveletmodwt).GetXml, MsgBoxStyle.Information)
正如您所见,上面显示的动态代码是shoal shell脚本的VB翻译版本!这真是太棒了!