C# 实现友元及方法调用限制

C#的范围限定符在大部分情况下,能够满足需求。但是考虑这样的情况,我们希望一个类的public方法能够被另外一个类的方法调用,但是又仅限于某些允许的方法来调用。通常的情形是这两个类的调用是单向的,但是又不合适使用嵌套类来定义以及将两个类合并。
对于C++来说,有Friend Class的定义来让两个类之间的方法能够相互暴露,而C#并没有类似的实现。
在没有语法和语言特性的支持的情况下,我们如果要对方法调用进行限制,可以使用StackFrame对调用进行检测。

考虑如下情况,我们有三个类A,B,C,我们希望A.CallingValidFromB 只能被B的方法调用,而其他类的方法调用该方法则会报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class A{
public static void CallingValidFromB(){
}
}

public class B{
public static void TestCall(){
A.CallingValidFromB(); //OK
}
}

public class C{
public static void TestCall(){
C.CallingValidFromB();// Error throw exception
}
}

对此,我们可以StackFrame对方法调用进行分析,来检测caller和callee是否满足要求。StackFrame可以获取到每个call stack中的每一步状态的方法。

方法调用限制检测

RestrictCaller方法会对caller进行校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public static class DevTool
{
public static bool RestrictCaller<T>(params string[] methods)
{
StackFrame frame1 = new StackFrame(1);
StackFrame frame2 = new StackFrame(2);

var callerMethod = frame2.GetMethod();
var callerType = callerMethod.DeclaringType;

var calleeMethod = frame1.GetMethod();
var calleeType = calleeMethod.DeclaringType;

var calleeId = $"{calleeType.FullName}.{calleeMethod.Name}()";

var expectedType = typeof(T);
if (expectedType != callerType)
{
UnityDebug.LogError($"Call Method:<{calleeId}> withType:<{callerType.FullName}>, expect:<{expectedType.FullName}>");
return false;
}

if (methods == null || methods.Length == 0) return true;
var methodList = new List<string>(methods);
if (!methodList.Contains(callerMethod.Name))
{
var callerId = $"{callerType.FullName}.{callerMethod.Name}()";
UnityDebug.LogError($"Call Method:<{calleeId}> withMethod:<{callerId}> is not permitted.");
return false;
}
return true;
}

这样对于上文的例子,我们就可以添加校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class A{
public static void CallingValidFromB(){
//限制方法从B调用而来
var callingValid = DevTool.RestrictCaller<B>();
if(!callingValid) return;

// Do something
}

public static void CallingValidFromBWithDefinedMethod(){
//限制方法只能从 B.TestCall() 中进行调用
var callingValid = DevTool.RestrictCaller<B>(nameof(B.TestCall));
if(!callingValid) return;

// Do something
}
}

快速获取CallingMethod

.NET 4.0之后System.Runtime.CompilerServices中提供了 三个Attribute用户快速获取Caller信息

1
2
3
[CallerFilePathAttribute]
[CallerMemberNameAttribute]
[CallerLineNumberAttribute]
1
2
3
4
5
6
7
8

public void TestMethod(int val,[CallerMemberName] string callerMethod =""){
Console.WriteLine($"caller method: {callerMethod}");
}

public void AnotherMethod(){
TestMethod(1); // caller method: AnotherMethod
}

使用接口隐藏方法

除去运行时调用检测之外,我们还可以使用接口将一些非公开的方法隐藏。在需要被暴露的类中直接将对象进行类型转换,再进行函数调用。

1
2
3
4
5
6
7
public interface A{
void publicMethod();
}

public class AWithInternal : A{
public void testMethod(){}
}