MyFramework 里有一个很小的扩展函数:
bash
safe()
它经常出现在这种代码里:
bash
foreach (var item in list.safe())
{
// ...
}
这个函数本身很简单,但它解决的是框架代码里非常高频的问题:
集合可能为空,但遍历逻辑不应该到处写 null 判断。
一、代码
List<T> 的实现:
bash
public class EmptyList<T>
{
public static List<T> mList;
public static List<T> getEmptyList()
{
mList ??= new();
return mList;
}
}
public static class ListExtension
{
public static List<T> safe<T>(this List<T> original)
{
return original ?? EmptyList<T>.getEmptyList();
}
}
数组也有对应实现:
bash
public class EmptyArray<T>
{
public static T[] mList;
public static T[] getEmptyList()
{
mList ??= new T[0];
return mList;
}
}
public static class ArrayExtension
{
public static T[] safe<T>(this T[] original)
{
return original ?? EmptyArray<T>.getEmptyList();
}
}
字典和 HashSet 也一样:
bash
public static Dictionary<TKey, TValue> safe<TKey, TValue>(this Dictionary<TKey, TValue> dic)
{
return dic ?? EmptyDictionary<TKey, TValue>.getEmptyList();
}
public static HashSet<T> safe<T>(this HashSet<T> original)
{
return original ?? EmptyHashSet<T>.getEmptyList();
}
二、用法
普通写法:
bash
if (callbackList != null)
{
foreach (var callback in callbackList)
{
callback();
}
}
使用 safe() 后:
bash
foreach (var callback in callbackList.safe())
{
callback();
}
链式调用时更明显:
bash
foreach (Type item in (mStateManager.getGroup(groupType)?.mStateList).safe())
{
// ...
}
如果 getGroup(groupType) 返回 null,后面的 mStateList 也是 null。
safe() 会把 null 集合转换成空集合。
遍历逻辑不需要关心中间对象是否存在。
三、核心作用
safe() 的作用不是让集合"安全可写"。
它的核心作用是:
bash
把 null 集合转换成空集合
这样遍历时可以统一写成:
bash
foreach (var item in list.safe())
{
}
而不是每次都写:
bash
if (list != null)
{
foreach (var item in list)
{
}
}
它减少的是框架代码里的重复分支。
四、为什么不用 new 空集合
可以这样写:
bash
return original ?? new List<T>();
但这样每次遇到 null 都会创建一个新对象。
safe() 使用的是泛型静态空集合:
bash
EmptyList<T>.getEmptyList()
每种 T 只创建一个空列表。
比如:
bash
EmptyList<int>
EmptyList<string>
EmptyList<GameObject>
它们各自有自己的静态空集合。
这样做的结果是:
bash
不会反复分配空 List
不会增加无意义 GC
空集合可以被重复用于遍历
数组的 EmptyArray<T> 也是一样。
五、适合场景
safe() 最适合只读遍历。
比如:
bash
foreach (Action item in mPositionModifyCallback.safe())
{
item?.Invoke();
}
或者:
bash
foreach (WindowObjectBase item in mChildList.safe())
{
item.setVisible(false);
}
或者:
bash
foreach (string pattern in excludePatterns.safe())
{
// ...
}
这些场景里,null 和空集合的语义基本一致:
bash
没有集合
等价于
没有元素需要处理
所以 safe() 很合适。
六、不是写入入口
safe() 返回的空集合是共享对象。
所以这类写法不应该出现:
bash
list.safe().Add(item);
如果 list 是 null,这里添加的是 EmptyList<T> 里的共享空列表。
这会污染后续所有同类型的空集合遍历。
所以 safe() 的使用边界很明确:
bash
用于遍历
用于读取
不用于写入
如果要写入,就应该先保证原集合存在:
bash
list ??= new();
list.Add(item);
这点是使用 safe() 时最重要的约束。
七、和 isEmpty 的区别
MyFramework 里也有:
bash
public static bool isEmpty<T>(this List<T> list)
{
return list == null || list.Count == 0;
}
isEmpty() 适合判断:
bash
if (list.isEmpty())
{
return;
}
safe() 适合遍历:
bash
foreach (var item in list.safe())
{
}
两者解决的问题不同。
isEmpty() 是分支判断。
safe() 是消除分支。
八、代码风格收益
在框架代码里,很多回调列表、组件列表、状态列表、子节点列表都可能为空。
如果每次遍历都写 null 判断,代码会很碎。
比如:
bash
if (mChildList != null)
{
foreach (var child in mChildList)
{
child.destroy();
}
}
这种代码出现几百次以后,会明显增加阅读噪音。
使用 safe() 后:
bash
foreach (var child in mChildList.safe())
{
child.destroy();
}
重点直接落在遍历逻辑上。
这也是它的价值。
它不是复杂设计,但很适合框架代码。
九、空对象思想
safe() 本质上是一种很小的空对象设计。
null 集合不是特殊分支,而是被转换成一个空集合对象。
bash
null List<T>
↓
EmptyList<T>
null T[]
↓
EmptyArray<T>
null Dictionary<TKey, TValue>
↓
EmptyDictionary<TKey, TValue>
null HashSet<T>
↓
EmptyHashSet<T>
这样调用者不需要关心 null。
只需要按正常集合遍历即可。
十、设计取舍
safe() 的优点很明显:
bash
减少 null 判断
减少重复代码
避免空集合反复分配
让遍历代码更统一
链式调用时更简洁
限制也很明确:
bash
只能用于读取和遍历
不能对返回的空集合写入
共享空集合不适合多线程写操作
不适合需要区分 null 和空集合语义的场景
所以它不是万能安全函数。
它是一个专门服务遍历场景的小工具。
总结
safe() 的实现很简单:
bash
return original ?? EmptyList<T>.getEmptyList();
但它在框架代码里很实用。
它把大量这种代码:
bash
if (list != null)
{
foreach (var item in list)
{
}
}
压缩成:
bash
foreach (var item in list.safe())
{
}
它的精巧点不在复杂,而在使用频率高、边界清楚、收益稳定。
在 MyFramework 里,safe() 适合用在大量只读遍历场景中。
它让 null 集合按空集合处理,减少分支,也避免重复创建临时空集合。
这就是 safe() 扩展函数的主要设计价值。