Effective C# 学习笔记(四十一)使利用DynamicObject或IDynamicMetaObjectProvider接口实现数据驱动的动态类型
2011-08-02 17:18
981 查看
实现动态行为可以帮助解决你编程实践中遇到的各种挑战。当要创建动态类型时,首先判断是否可以通过集成System.Dynamic.DynamicObject来实现。若不能通过继承的方式实现,就要实现IDynamicMetaObjectProvider接口,通过创建继承自DynamicMetaObject类型的嵌套类并重载其对应方法来实现对你要创建的动态类型的属性访问、方法调用等各种动态行为。其中用到了较为复杂的Expression定义,这层抽象较难理解,也较容易出错,请注意这部分逻辑的覆写。另外,用你自己的动态逻辑也要考虑的性能损失的问题,因为动态编程本来就要比静态编程消耗更多的资源,有得必有失么:)
动态编程的一个强大的能力就是可以让你在运行时创建接口随环境改变的类型。在C#中你可以使用以下两种方法实现这个效果。
继承System.Dynamic.DynamicObject基类
首先,看一个例子,加入你有一个类型,其的属性(Properties)是在运行时动态添加的,你可以如下定义这个类型。代码如下:
class
DynamicPropertyBag : DynamicObject
{
private
Dictionary<string, object> storage = new Dictionary<string,
object>();
public
override bool TryGetMember(GetMemberBinder binder, out object result)
{
if
(storage.ContainsKey(binder.Name))
{
result
= storage[binder.Name];
return
true;
}
result
= null;
return
false;
}
public
override bool TrySetMember(SetMemberBinder binder, object value)
{
string
key = binder.Name;
if
(storage.ContainsKey(key))
storage[key]
= value;
else
storage.Add(key,
value);
return
true;
}
public
override string ToString()
{
StringWriter
message = new StringWriter();
foreach
(var item in storage)
message.WriteLine("{0}:\t{1}",
item.Key, item.Value);
return
message.ToString();
}
}
//使用动态类型属性
dynamic
dynamicProperties = new DynamicPropertyBag();
try
{
//尝试获取不存在的属性
Console.WriteLine(dynamicProperties.Marker);
}
catch
(Microsoft.CSharp.RuntimeBinder.RuntimeBinderException)
{
Console.WriteLine("There
are no properties");
}
//动态添加属性
dynamicProperties.Date
= DateTime.Now;
dynamicProperties.Name
= "Bill Wagner";
dynamicProperties.Title
= "Effective C#";
dynamicProperties.Content
= "Building a dynamic dictionary";
//输出动态属性
Console.WriteLine(dynamicProperties);
DynamicObject类型包含了类似TrySetMember的方法来处理索引、方法、构造器、一元二元运算的调用方法。你可以自己重载这些方法,来构建你自己的动态逻辑。你要遵守的原则是调用方法时要检查被调用的是哪个Binder对象,处理相关的逻辑。当有返回值时,或有输出变量时,对返回值或输出变量(out parameter)赋值。
再看一例,下面定义了一个行星的XML数据,其中有的行星有像月亮的卫星,而有的没有,这时你在处理这样的XML的时候便免不了做节点丢失,缺少属性的判断:
<Planets>
<Planet>
<Name>Mercury</Name>
</Planet>
<Planet>
<Name>Venus</Name>
</Planet>
<Planet>
<Name>Earth</Name>
<Moons>
<Moon>Moon</Moon>
</Moons>
</Planet>
<Planet>
<Name>Mars</Name>
<Moons>
<Moon>Phobos</Moon>
<Moon>Deimos</Moon>
</Moons>
</Planet>
<!--
other data elided -->
</Planets>
//用Linq to XML操作上面的数据
//
Create an XElement document containing
//
solar system data:
var
xml = createXML();
var
firstPlanet = xml.Element("Planet");
var
earth = xml.Elements("Planet").Skip(2).First();
var
moon =
xml.Elements("Planet").Skip(2).First().Elements("Moons").First().Element("Moon");
上面的代码获取某个节点值是如此的麻烦,而且并不直观。这其实是在处理一个数据驱动的业务模型,你可以通过继承DynamicObject类型来处理这样的XML数据操作,对于数据XML中不存在的节点DynamicElement返回空值。如下代码所示:
//创建一个继承自dynamicObject类型的Xml元素处理类型。
public
class DynamicXElement : DynamicObject
{
private
readonly XElement xmlSource;
public
DynamicXElement(XElement source)
{
xmlSource
= source;
}
public
override bool TryGetMember(GetMemberBinder binder,out object result)
{
result
= new DynamicXElement(null);
if
(binder.Name == "Value")
{
result
= (xmlSource != null) ? xmlSource.Value : "";
return
true;
}
if
(xmlSource != null)
result
= new DynamicXElement(xmlSource.Element(XName.Get(binder.Name)));
//上面这句动态获取了XML节点属性对象,并又再次调用DynamicXElement类型构建递归查找对象
return
true;
}
public
override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object
result)
{
result
= null;
//
This only supports [string, int] indexers
if
(indexes.Length != 2)
return
false;
if
(!(indexes[0] is string))
return
false;
if
(!(indexes[1] is int))
return
false;
var
allNodes = xmlSource.Elements(indexes[0].ToString());
int
index = (int)indexes[1];
if
(index < allNodes.Count())
result
= new DynamicXElement(allNodes.ElementAt(index));
else
result
= new DynamicXElement(null);
return
true;
}
public
override string ToString()
{
if
(xmlSource != null)
return
xmlSource.ToString();
else
return
string.Empty;
}
}
//使用该XML动态处理类型操作XML数据
//
Create an XElement document containing
//
solar system data:
var
xml = createXML();
Console.WriteLine(xml);
dynamic
dynamicXML = new DynamicXElement(xml);
//
old way:
var
firstPlanet = xml.Element("Planet");
Console.WriteLine(firstPlanet);
//
new way:
//
returns the first planet.
dynamic
test2 = dynamicXML.Planet;
//
gets the third planet (Earth)
dynamic
test3 = dynamicXML["Planet", 2];
dynamic
earthMoon = dynamicXML["Planet", 2]["Moons", 0].Moon;
dynamic
test6 = dynamicXML["Planet", 2]["Moons", 3].Moon; // earth
doesn't have 4 moons
dynamic
fail = dynamicXML.NotAppearingInThisFile;
dynamic
fail2 = dynamicXML.Not.Appearing.In.This.File;
实现System.Dynamic.IDynamicMetaObjectProvider接口
由于dynamic类型在运行时是动态创建对象的,不会有编译时的静态类型,所以对该类型的每个成员的访问都会调用GetMetaObject方法,以获取动态对象,然后通过该动态对象进行调用,不论你是第几次调用,该方法是否是静态方法。所以实现IDynamicMetaObjectProvider接口,你需要实现一个GetMetaObject方法来返回DynamicMetaObject对象。如下代码所示:
class
DynamicDictionary2 : IDynamicMetaObjectProvider
{
#region IDynamicMetaObjectProvider
Members
DynamicMetaObject
IDynamicMetaObjectProvider.GetMetaObject(System.Linq.Expressions.Expression
parameter)
{
return new
DynamicDictionaryMetaObject(parameter, this);
}
#endregion
//这里定义了一个嵌套类DynamicDictionaryMetaObject ,其继承自DynamicMetaObject类型,并通过重载该类型的BindGetMember、BindSetMember、BindInvokeMember方法实现了对DynamicDictionary2
的GetDictionaryEntry及SetDictionaryEntry方法的动态访问
class DynamicDictionaryMetaObject :
DynamicMetaObject
{
internal
DynamicDictionaryMetaObject(Expression parameter, DynamicDictionary2
dynamicDictionary)
: base(parameter,
BindingRestrictions.Empty, dynamicDictionary)
{
}
public override DynamicMetaObject
BindSetMember(SetMemberBinder binder, DynamicMetaObject value)
{
// Method to call in the
containing class:
string methodName =
"SetDictionaryEntry";
// setup the binding
restrictions.
BindingRestrictions
restrictions = BindingRestrictions.GetTypeRestriction(Expression, LimitType);
// setup the parameters:
Expression[] args = new
Expression[2];
// First parameter is the name
of the property to Set
args[0] =
Expression.Constant(binder.Name);
// Second parameter is the
value
args[1] =
Expression.Convert(value.Expression, typeof(object));
// Setup the 'this' reference
Expression self =
Expression.Convert(Expression, LimitType);
// Setup the method call
expression
Expression methodCall =
Expression.Call(self, typeof(DynamicDictionary2).GetMethod(methodName), args);
// Create a meta object to
invoke Set later:
DynamicMetaObject
setDictionaryEntry = new DynamicMetaObject(methodCall, restrictions);
// return that dynamic object
return setDictionaryEntry;
}
public override DynamicMetaObject
BindGetMember(GetMemberBinder binder)
{
// Method call in the
containing class:
string methodName =
"GetDictionaryEntry";
// One parameter
Expression[] parameters = new
Expression[]
{
Expression.Constant(binder.Name)
};
DynamicMetaObject
getDictionaryEntry = new DynamicMetaObject(
Expression.Call(
Expression.Convert(Expression, LimitType),
typeof(DynamicDictionary2).GetMethod(methodName),
parameters),
BindingRestrictions.GetTypeRestriction(Expression, LimitType));
return getDictionaryEntry;
}
public override DynamicMetaObject
BindInvokeMember(InvokeMemberBinder binder, DynamicMetaObject[] args)
{
StringBuilder paramInfo = new
StringBuilder();
paramInfo.AppendFormat("Calling {0}(",
binder.Name);
foreach (var item in args)
paramInfo.AppendFormat("{0}, ",
item.Value);
paramInfo.Append(")");
Expression[]
parameters = new Expression[]
{
Expression.Constant(paramInfo.ToString())
};
DynamicMetaObject methodInfo =
new DynamicMetaObject(
Expression.Call(
Expression.Convert(Expression, LimitType),
typeof(DynamicDictionary2).GetMethod("WriteMethodInfo"),
parameters),
BindingRestrictions.GetTypeRestriction(Expression, LimitType));
return methodInfo;
}
}
private Dictionary<string,
object> storage = new Dictionary<string, object>();
public object SetDictionaryEntry(string
key, object value)
{
if (storage.ContainsKey(key))
storage[key] = value;
else
storage.Add(key, value);
return value;
}
public object GetDictionaryEntry(string
key)
{
object result = null;
if (storage.ContainsKey(key))
{
result = storage[key];
}
return result;
}
public override string ToString()
{
StringWriter message = new
StringWriter();
foreach (var item in storage)
message.WriteLine("{0}:\t{1}", item.Key,
item.Value);
return message.ToString();
}
public object WriteMethodInfo(string
methodInfo)
{
Console.WriteLine(methodInfo);
return 42; // because it is the answer to everything
}
}
注意:SetDictionary方法将赋值语句的右侧作为返回值返回,以保证类似如下的等式传递可用:
DateTime
current = propertyBag2.Date = DateTime.Now;
注意:在使用赋值操作时,需要对赋值的值进行校验,以保证其是一个合法的赋值,像如下的语句的调用就是你为什么需要Restriction的原因。
propertyBag2.MagicNumber
= GetMagicNumber();
另外,用反射获取dynamic对象的属性是不成立的,例如如下代码所示:
dynamic
expando = new ExpandoObject();
expando.SampleProperty
= "This property was added at run time";
PropertyInfo
dynamicProperty = expando.GetType().GetProperty("SampleProperty");
//
dynamicProperty is null.
由于dynamic声明的对象未必都实现了IDynamicMetaObjectProvider接口,因此,如果将动态功能与反射一起使用,则请记住,反射不适用于动态添加的属性和方法,并且最好检查正在反射的对象是否实现了
IDynamicMetaObjectProvider 接口。 如下代码所示:
dynamic
expando = new ExpandoObject();
Console.WriteLine(expando
is IDynamicMetaObjectProvider);
//
True
dynamic
test = "test";
Console.WriteLine(test
is IDynamicMetaObjectProvider);
//
False
对与数据驱动的业务逻辑,如在处理JSON或XML格式的数据的时候,你可以少创建些对应这些数据的静态类,而使用动态类型来处理这些数据,而且可以得到更好的异常处理方式,不会为数据的不完整添加更多的验证逻辑。Phil
Haack在它的Fun
With Method Missing and C# 4中描述了他如何在C#4.0中实现了method_missing (from
Ruby),有空可以看下,受受启发。
动态编程的一个强大的能力就是可以让你在运行时创建接口随环境改变的类型。在C#中你可以使用以下两种方法实现这个效果。
继承System.Dynamic.DynamicObject基类
首先,看一个例子,加入你有一个类型,其的属性(Properties)是在运行时动态添加的,你可以如下定义这个类型。代码如下:
class
DynamicPropertyBag : DynamicObject
{
private
Dictionary<string, object> storage = new Dictionary<string,
object>();
public
override bool TryGetMember(GetMemberBinder binder, out object result)
{
if
(storage.ContainsKey(binder.Name))
{
result
= storage[binder.Name];
return
true;
}
result
= null;
return
false;
}
public
override bool TrySetMember(SetMemberBinder binder, object value)
{
string
key = binder.Name;
if
(storage.ContainsKey(key))
storage[key]
= value;
else
storage.Add(key,
value);
return
true;
}
public
override string ToString()
{
StringWriter
message = new StringWriter();
foreach
(var item in storage)
message.WriteLine("{0}:\t{1}",
item.Key, item.Value);
return
message.ToString();
}
}
//使用动态类型属性
dynamic
dynamicProperties = new DynamicPropertyBag();
try
{
//尝试获取不存在的属性
Console.WriteLine(dynamicProperties.Marker);
}
catch
(Microsoft.CSharp.RuntimeBinder.RuntimeBinderException)
{
Console.WriteLine("There
are no properties");
}
//动态添加属性
dynamicProperties.Date
= DateTime.Now;
dynamicProperties.Name
= "Bill Wagner";
dynamicProperties.Title
= "Effective C#";
dynamicProperties.Content
= "Building a dynamic dictionary";
//输出动态属性
Console.WriteLine(dynamicProperties);
DynamicObject类型包含了类似TrySetMember的方法来处理索引、方法、构造器、一元二元运算的调用方法。你可以自己重载这些方法,来构建你自己的动态逻辑。你要遵守的原则是调用方法时要检查被调用的是哪个Binder对象,处理相关的逻辑。当有返回值时,或有输出变量时,对返回值或输出变量(out parameter)赋值。
再看一例,下面定义了一个行星的XML数据,其中有的行星有像月亮的卫星,而有的没有,这时你在处理这样的XML的时候便免不了做节点丢失,缺少属性的判断:
<Planets>
<Planet>
<Name>Mercury</Name>
</Planet>
<Planet>
<Name>Venus</Name>
</Planet>
<Planet>
<Name>Earth</Name>
<Moons>
<Moon>Moon</Moon>
</Moons>
</Planet>
<Planet>
<Name>Mars</Name>
<Moons>
<Moon>Phobos</Moon>
<Moon>Deimos</Moon>
</Moons>
</Planet>
<!--
other data elided -->
</Planets>
//用Linq to XML操作上面的数据
//
Create an XElement document containing
//
solar system data:
var
xml = createXML();
var
firstPlanet = xml.Element("Planet");
var
earth = xml.Elements("Planet").Skip(2).First();
var
moon =
xml.Elements("Planet").Skip(2).First().Elements("Moons").First().Element("Moon");
上面的代码获取某个节点值是如此的麻烦,而且并不直观。这其实是在处理一个数据驱动的业务模型,你可以通过继承DynamicObject类型来处理这样的XML数据操作,对于数据XML中不存在的节点DynamicElement返回空值。如下代码所示:
//创建一个继承自dynamicObject类型的Xml元素处理类型。
public
class DynamicXElement : DynamicObject
{
private
readonly XElement xmlSource;
public
DynamicXElement(XElement source)
{
xmlSource
= source;
}
public
override bool TryGetMember(GetMemberBinder binder,out object result)
{
result
= new DynamicXElement(null);
if
(binder.Name == "Value")
{
result
= (xmlSource != null) ? xmlSource.Value : "";
return
true;
}
if
(xmlSource != null)
result
= new DynamicXElement(xmlSource.Element(XName.Get(binder.Name)));
//上面这句动态获取了XML节点属性对象,并又再次调用DynamicXElement类型构建递归查找对象
return
true;
}
public
override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object
result)
{
result
= null;
//
This only supports [string, int] indexers
if
(indexes.Length != 2)
return
false;
if
(!(indexes[0] is string))
return
false;
if
(!(indexes[1] is int))
return
false;
var
allNodes = xmlSource.Elements(indexes[0].ToString());
int
index = (int)indexes[1];
if
(index < allNodes.Count())
result
= new DynamicXElement(allNodes.ElementAt(index));
else
result
= new DynamicXElement(null);
return
true;
}
public
override string ToString()
{
if
(xmlSource != null)
return
xmlSource.ToString();
else
return
string.Empty;
}
}
//使用该XML动态处理类型操作XML数据
//
Create an XElement document containing
//
solar system data:
var
xml = createXML();
Console.WriteLine(xml);
dynamic
dynamicXML = new DynamicXElement(xml);
//
old way:
var
firstPlanet = xml.Element("Planet");
Console.WriteLine(firstPlanet);
//
new way:
//
returns the first planet.
dynamic
test2 = dynamicXML.Planet;
//
gets the third planet (Earth)
dynamic
test3 = dynamicXML["Planet", 2];
dynamic
earthMoon = dynamicXML["Planet", 2]["Moons", 0].Moon;
dynamic
test6 = dynamicXML["Planet", 2]["Moons", 3].Moon; // earth
doesn't have 4 moons
dynamic
fail = dynamicXML.NotAppearingInThisFile;
dynamic
fail2 = dynamicXML.Not.Appearing.In.This.File;
实现System.Dynamic.IDynamicMetaObjectProvider接口
由于dynamic类型在运行时是动态创建对象的,不会有编译时的静态类型,所以对该类型的每个成员的访问都会调用GetMetaObject方法,以获取动态对象,然后通过该动态对象进行调用,不论你是第几次调用,该方法是否是静态方法。所以实现IDynamicMetaObjectProvider接口,你需要实现一个GetMetaObject方法来返回DynamicMetaObject对象。如下代码所示:
class
DynamicDictionary2 : IDynamicMetaObjectProvider
{
#region IDynamicMetaObjectProvider
Members
DynamicMetaObject
IDynamicMetaObjectProvider.GetMetaObject(System.Linq.Expressions.Expression
parameter)
{
return new
DynamicDictionaryMetaObject(parameter, this);
}
#endregion
//这里定义了一个嵌套类DynamicDictionaryMetaObject ,其继承自DynamicMetaObject类型,并通过重载该类型的BindGetMember、BindSetMember、BindInvokeMember方法实现了对DynamicDictionary2
的GetDictionaryEntry及SetDictionaryEntry方法的动态访问
class DynamicDictionaryMetaObject :
DynamicMetaObject
{
internal
DynamicDictionaryMetaObject(Expression parameter, DynamicDictionary2
dynamicDictionary)
: base(parameter,
BindingRestrictions.Empty, dynamicDictionary)
{
}
public override DynamicMetaObject
BindSetMember(SetMemberBinder binder, DynamicMetaObject value)
{
// Method to call in the
containing class:
string methodName =
"SetDictionaryEntry";
// setup the binding
restrictions.
BindingRestrictions
restrictions = BindingRestrictions.GetTypeRestriction(Expression, LimitType);
// setup the parameters:
Expression[] args = new
Expression[2];
// First parameter is the name
of the property to Set
args[0] =
Expression.Constant(binder.Name);
// Second parameter is the
value
args[1] =
Expression.Convert(value.Expression, typeof(object));
// Setup the 'this' reference
Expression self =
Expression.Convert(Expression, LimitType);
// Setup the method call
expression
Expression methodCall =
Expression.Call(self, typeof(DynamicDictionary2).GetMethod(methodName), args);
// Create a meta object to
invoke Set later:
DynamicMetaObject
setDictionaryEntry = new DynamicMetaObject(methodCall, restrictions);
// return that dynamic object
return setDictionaryEntry;
}
public override DynamicMetaObject
BindGetMember(GetMemberBinder binder)
{
// Method call in the
containing class:
string methodName =
"GetDictionaryEntry";
// One parameter
Expression[] parameters = new
Expression[]
{
Expression.Constant(binder.Name)
};
DynamicMetaObject
getDictionaryEntry = new DynamicMetaObject(
Expression.Call(
Expression.Convert(Expression, LimitType),
typeof(DynamicDictionary2).GetMethod(methodName),
parameters),
BindingRestrictions.GetTypeRestriction(Expression, LimitType));
return getDictionaryEntry;
}
public override DynamicMetaObject
BindInvokeMember(InvokeMemberBinder binder, DynamicMetaObject[] args)
{
StringBuilder paramInfo = new
StringBuilder();
paramInfo.AppendFormat("Calling {0}(",
binder.Name);
foreach (var item in args)
paramInfo.AppendFormat("{0}, ",
item.Value);
paramInfo.Append(")");
Expression[]
parameters = new Expression[]
{
Expression.Constant(paramInfo.ToString())
};
DynamicMetaObject methodInfo =
new DynamicMetaObject(
Expression.Call(
Expression.Convert(Expression, LimitType),
typeof(DynamicDictionary2).GetMethod("WriteMethodInfo"),
parameters),
BindingRestrictions.GetTypeRestriction(Expression, LimitType));
return methodInfo;
}
}
private Dictionary<string,
object> storage = new Dictionary<string, object>();
public object SetDictionaryEntry(string
key, object value)
{
if (storage.ContainsKey(key))
storage[key] = value;
else
storage.Add(key, value);
return value;
}
public object GetDictionaryEntry(string
key)
{
object result = null;
if (storage.ContainsKey(key))
{
result = storage[key];
}
return result;
}
public override string ToString()
{
StringWriter message = new
StringWriter();
foreach (var item in storage)
message.WriteLine("{0}:\t{1}", item.Key,
item.Value);
return message.ToString();
}
public object WriteMethodInfo(string
methodInfo)
{
Console.WriteLine(methodInfo);
return 42; // because it is the answer to everything
}
}
注意:SetDictionary方法将赋值语句的右侧作为返回值返回,以保证类似如下的等式传递可用:
DateTime
current = propertyBag2.Date = DateTime.Now;
注意:在使用赋值操作时,需要对赋值的值进行校验,以保证其是一个合法的赋值,像如下的语句的调用就是你为什么需要Restriction的原因。
propertyBag2.MagicNumber
= GetMagicNumber();
另外,用反射获取dynamic对象的属性是不成立的,例如如下代码所示:
dynamic
expando = new ExpandoObject();
expando.SampleProperty
= "This property was added at run time";
PropertyInfo
dynamicProperty = expando.GetType().GetProperty("SampleProperty");
//
dynamicProperty is null.
由于dynamic声明的对象未必都实现了IDynamicMetaObjectProvider接口,因此,如果将动态功能与反射一起使用,则请记住,反射不适用于动态添加的属性和方法,并且最好检查正在反射的对象是否实现了
IDynamicMetaObjectProvider 接口。 如下代码所示:
dynamic
expando = new ExpandoObject();
Console.WriteLine(expando
is IDynamicMetaObjectProvider);
//
True
dynamic
test = "test";
Console.WriteLine(test
is IDynamicMetaObjectProvider);
//
False
对与数据驱动的业务逻辑,如在处理JSON或XML格式的数据的时候,你可以少创建些对应这些数据的静态类,而使用动态类型来处理这些数据,而且可以得到更好的异常处理方式,不会为数据的不完整添加更多的验证逻辑。Phil
Haack在它的Fun
With Method Missing and C# 4中描述了他如何在C#4.0中实现了method_missing (from
Ruby),有空可以看下,受受启发。
相关文章推荐
- 自己在项目中的学习总结:利用工厂模式+反射机制+缓存机制,实现动态创建不同的数据层对象接口
- Effective C# 学习笔记(三十一)利用IComparable<T>和IComparer<T>接口来实现排序关系
- [学习笔记]利用Jquery实现一些动态效果
- 【JavaEE学习笔记】Hibernate_05_数据类型转换和大对象处理,QBC(junit),DAO接口
- JAVA学习笔记Day25——动态接口的实现
- 【Spring学习笔记-MVC-5】利用spring MVC框架,实现ajax异步请求以及json数据的返回
- 用ObjectDataProvider绑定方法,用IValueConverter实现数据类型转换,重载ValidationRule实现数据验证,BindsDirectlyToSource等
- flash shareobject能实现跨域数据共享吗?(学习笔记)
- 【CI学习笔记】利用jquery中的ajax,调用接口,实现登录
- C# 学习笔记(四) 结构体实现接口后是值类型还是引用类型
- wince驱动学习笔记(vs2005实现流驱动动态加载与卸载 1)
- wince驱动学习笔记(vs2005实现流驱动动态加载与卸载 2)
- 利用meta-data的数据,动态更改contetnprovider的authorities
- Object-C语法学习笔记(一)——数据类型
- wince驱动学习笔记(vs2005实现流驱动动态加载与卸载 1)
- Java学习笔记——利用接口和observer实现对象监视
- Effective C# 学习笔记(二十二)多用接口定义实现,少用继承
- Object-C学习笔记(一)---数据类型
- 利用Python如何实现数据驱动的接口自动化测试
- 【数据结构与算法学习笔记】PART2 向量(接口与实现,可扩充向量,无序向量,有序向量)