C# 函数扩展方法的妙用
扩展方法 Extension Method 我们很多时候都是考虑方便性才去添加的, 系统库也有很多, 像 Linq / Expression 之类的, 使用起来就像是给对象添加了一个成员函数一样 :
官方例子
namespace ExtensionMethods { public static class IntExtensions { public static bool IsGreaterThan(this int i, int value) { return i > value; } } }
using ExtensionMethods; class Program { static void Main(string[] args) { int i = 10; bool result = i.IsGreaterThan(100); Console.WriteLine(result); } }
看到扩展的函数调用就像成员变量一样, 不过真的是这样吗? 看看下面的代码 :
using UnityEngine; using System; public class Test : MonoBehaviour { private System.Action aCall = null; private void Start() { aCall.Call(); } } public static class Ext { public static void Call(this System.Action action) { if(action != null) { action.Invoke(); } } }
断点看, 能够进来 :
那么它提供的扩展就不是代码层面的, 是编译层面的了, 在编译上所有的方法都是静态的, 只是在调用的时候传入了调用对象, 而成员函数只是在上面进行的封装, 从反射的Method.Invoke() 就能看到实例需要传入对象才能正确调用 :
public object Invoke(object obj, object[] parameters)
其实它的代码等效于 :
aCall.Call(); // 等于 Ext.Call(aCall);
所以就算看起来是成员函数调用, 其实是静态调用, 所以即使对象 aCall 是空, 也是可以运行的, 对于 Unity 来说, 很多时候会发生非预期的对象删除, 或者删除后仍然存在的现象, 每次都需要判空, 就像一个 UI 上的 Text 这样 :
public class Test : MonoBehaviour { public Text title; public void SetTitle(string info) { if(title) { title.text = info; } } }
这样只在功能内写判空的就比较累人, 不如写个静态方法 :
public class Test : MonoBehaviour { public UnityEngine.UI.Text title; } public static class Ext { public static void SetTextUI(UnityEngine.UI.Text text, string info) { if(text) { text.text = info; } } } //... Text textUI; Ext.SetTextUI(textUI, "xxx");
不过现在发现扩展方法的调用也是静态调用, 空对象也能运行, 那就写成扩展就更方便了 :
public static class Ext { public static void SetTextUI(this UnityEngine.UI.Text text, string info) { if(text) { text.text = info; } } } //... Text textUI; textUI.SetTextUI("xxx");
这就是扩展方法的好处了, 它不是代码层面的添加了一个成员函数.
还有一个现在用 C# 6.0 以上语法的话, 可以直接判空 :
Text textUI; textUI?.text = "xxx";
可是对于 Unity Object 对象, 这样的判空相当于 :
Text textUI; if(textUI != null) { textUI.text = "xxx"; }
这样判空是不对的, 必须使用它的隐式转换 bool 来判断, 想要这个功能的正确实现, 只有通过修改语法树的方法来尝试了...
补充个常用方案:
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public static void SetText_W(this Text text, string info) { #if UNITY_EDITOR text.text = info; #else if(text) { text.text = info; } #endif }
像这样的, 建议编译器进行inline编译, 可以减少函数开销, 然后在开发阶段还是会报错, 能够在开发时候发现错误, 不过在发布之后就是带有检测的代码, 就算出错也不影响代码执行.