最近在使用反射获取函数的签名时,发现了一类后缀带&的类型,比如String&, Int32&。
一开始我以为这是表示这些基础类型的引用类型,但是明明已经有了int?这样的可空类型,那么为什么还要有一类"&"后缀的类型呢?
不如先谈谈是如何获得这类类型的吧。
======================== 楔子 ==================================
如果声明了这样一个函数
class Program
{
private static bool TryParse(string source, out int result)
{
if (string.IsNullOrEmpty(source))
{
result = 0;
return false;
}
result = 204; // 随便一个数字, 仅做伪代码
return true;
}
}
那么获取这个函数以及他的签名参数
// 获得函数
var method = typeof(Program).GetMethod("TryParse",
BindingFlags.NonPublic| BindingFlags.Static);
// 获得签名参数
var parameterInfos = method.GetParameters();
// 在显而易见的情况下, 这个函数拥有二个参数
var sourceParameter = parameterInfos[0];
var outIntParameter = parameterInfos[1];
第一个参数没有什么特别的地方,他的类型肯定是string无疑,他的sourceParameter.ParameterType.Name 输出的值也是string
但是第二个参数的类型的名字则是"Int32&",显然他不是可空类型,那么可以根据他的用途推测下他的类型,没错,他就是表明该类型是用于"传递的"。
根据C#里,对 ref 和 out 使用描述可以得出,在参数前加上这两个修饰符,相当于对该参数的地址直接进行操作,详见这两个关键字的描述,这里仅做引入概念。
在Type这个类下有个实例属性: public bool IsByRef {get;} 用来表示这个类型是否为传递类型(而不是表示引用类型)。
假如函数声明为
class Program
{
public class IntWrapper
{
public int Value { get; set;}
}
private static void DoSomeThing(IntWrapper result)
{
result = new IntWrapper();
result.Value = 2;
}
static void Main(string[] args)
{
var wrapper = new IntWrapper();
wrapper.Value = 0;
DoSomeThing(wrapper);
Console.WriteLine(wrapper.Value);
// 0
}
}
如果我们直接修改了result的值,那么函数外部这个Wrapper的值并不会变,这和是否为引用类型无关(这是个基础知识,温习一下),只有当DoSomeThing的参数加上ref修饰符时,这个result改变了,外部的wrapper才会跟着改变。
因此 {className}& 表示的是一个传递类型,这个类型无法直接通过typeof({className}&)声明获得(编译器会报错),但是可以根据Type.GetType("String&")或其他Type.GetType($"{className}&,{assemblyName}")获得相应类型的类型。
另外,获取常规类型后,可以通过Type的实例方法MakeByRefType()使之变为传递类型。
======================== ParameterInfo ============================
前文中,涉及到一个类叫ParameterInfo。之前仅仅从Type下的实例属性IsByRef只能知道这个类型表示的是传递关系,但是不知道他是用来标记 ref 还是 out 的。因为表示这个参数是否为 out 标记,是"参数"的"属性",因此在method.GetParameters()获取的参数列表中,其相对应的参数的IsOut为true则表示修饰符为out,否则为ref(前提是该参数的类型的IsByRef为true)。
======================== 扩展使用 ================================
折腾了这么多,有什么实际意义呢,答案似乎没有想象那么有意义,只是让你看见这个类型不再迷惑。
我们都知道,C#中函数是可以重载的,只有签名(也就是参数个数,类型不再一致)不同时才可以重载(但是如果相同的参数,仅有 ref 和 out 对其中的参数进行修饰是不可的)。
class Program
{
private static void PrintHelloWorld(string name)
{
// do sth
}
// 编译器会报错
private static void PrintHelloWorld(ref string name)
{
// do sth
}
}
那么我们获得一个函数其中很强大的一个方法是直接根据函数的签名类型进行反射,
Type的实例方法GetMethod(string name, BindingFlags bindingAttr, Binder binder, Type[] types, ParameterModifier[] modifiers) 这个方法则可以更加精确获取你所想获得的函数。
通过反射调用参数类型为 ref 或 out 的函数时, 如上个代码中的PrintHelloWorld(下面那个参数为 ref 修饰的),
class Program
{
// 后续会用到
private delegate void PrintHelleWorldDelegate(ref string arg);
private static void PrintHelloWorld(ref string name)
{
name = "Tom";
}
static void Main(string[] args)
{
var refStringType = Type.GetType("System.String&, mscorlib");
var method = typeof(Program).GetMethod("PrintHelloWorld",
BindingFlags.Static | BindingFlags.NonPublic,
null, new[] { refStringType }, null);
var name = "Jerry";
var content = new object[] { name };
method.Invoke(null, content);
name = (string)content[0];
Console.WriteLine(name);
// Tom
}
}
唔,调用起来看起来不太方便呢,其实Delegate.CreateDelegate可以把一个MethodInfo包装为指定类型的委托。
如果无法用typeof()获取到传递类型的实例一样, Action和Func在此时同样失效了,因此只能使用delegate关键字来声明上文中的PrintHelloWorldDelegate类型了。
那么
static void Main(string[] args)
{
var refStringType = Type.GetType("System.String&, mscorlib");
var method = typeof(Program).GetMethod("PrintHelloWorld",
BindingFlags.Static | BindingFlags.NonPublic,
null, new[] { refStringType }, null);
var name = "Jerry";
var orderedMethod =
Delegate.CreateDelegate(typeof(PrintHelleWorldDelegate), method)
as PrintHelleWorldDelegate;
// 此时,调用方法和原函数一致
orderedMethod.Invoke(ref name);
Console.WriteLine(name);
// Tom
}
这样调用起来就方便多了。
以上,就是目前根据"String&"引申而来的内容。
实际的用途还需要实践才能知道,目前就先把这部分当做知识点记录下来。