目录
(2)示例:PlayerInfo 中的 Dictionary 字段
十三、项目示例:结合反射——读取Dictionary数据类型数据
一、PlayerPrefs
1.本质
基于键值对的轻量级数据存储方式
2.存储位置
不同平台有不同的存储路径(通常在系统的注册表或配置文件夹中)
3.支持类型
仅原生支持 int、float、string 三种类型
4.常用 API
PlayerPrefs.SetInt("key", 100); // 存储整数
PlayerPrefs.SetFloat("key", 3.14f); // 存储浮点数
PlayerPrefs.SetString("key", "test"); // 存储字符串
int i = PlayerPrefs.GetInt("key"); // 读取整数
PlayerPrefs.DeleteKey("key"); // 删除键值
PlayerPrefs.Save(); // 强制保存
PlayerPrefs.DeleteAll(); // 清除所有数据
二、PlayerPrefsDataMgr工具类 核心原理
该工具类解决了原生 PlayerPrefs 的两个主要痛点:
(1)手动管理大量键名容易冲突
(2)不直接支持自定义类和 bool 等类型的存储
1.单例模式设计
采用单例模式确保全局只有一个数据管理器实例:
private static PlayerPrefsDataMgr instance = new PlayerPrefsDataMgr();
public static PlayerPrefsDataMgr Instance { get { return instance; } }
private PlayerPrefsDataMgr() { } // 私有构造函数防止外部实例化
2.智能键名生成规则
(1)格式
通过反射自动生成唯一键名,:
[对象标识]_[对象类型]_[字段类型]_[字段名]
例如:Player1_PlayerInfo_int_age
(2)命名规则的优势
【1】避免键名冲突
【2】便于调试和维护
【3】支持同一类型的多个实例存储(如多个玩家数据)
3.数据存储实现
通过反射遍历对象字段,根据字段类型自动选择合适的存储方式。
public void SaveData(object data, string keyName)
{
Type dataType = data.GetType();
FieldInfo[] fields = dataType.GetFields(); // 获取所有字段信息
foreach (var field in fields)
{
// 生成唯一键名
string saveKey = $"{keyName}_{dataType.Name}_{field.FieldType.Name}_{field.Name}";
// 根据字段类型存储值
SaveValue(field.GetValue(data), saveKey);
}
}
// 类型适配存储
private void SaveValue(object value, string key)
{
Type type = value.GetType();
if (type == typeof(int)) PlayerPrefs.SetInt(key, (int)value);
else if (type == typeof(float)) PlayerPrefs.SetFloat(key, (float)value);
else if (type == typeof(string)) PlayerPrefs.SetString(key, value.ToString());
else if (type == typeof(bool)) PlayerPrefs.SetInt(key, (bool)value ? 1 : 0); // bool特殊处理
}
三、数据读取功能实现
public object LoadData(Type type, string keyName)
{
// 创建目标类型实例
object data = Activator.CreateInstance(type);
// 获取所有字段信息
FieldInfo[] fields = type.GetFields();
foreach (var field in fields)
{
// 按照存储规则生成键名
string loadKey = $"{keyName}_{type.Name}_{field.FieldType.Name}_{field.Name}";
// 读取值并设置到字段
field.SetValue(data, LoadValue(field.FieldType, loadKey));
}
return data;
}
// 根据类型读取对应的值
private object LoadValue(Type fieldType, string key)
{
if (!PlayerPrefs.HasKey(key)) return GetDefaultValue(fieldType); // 处理键不存在的情况
if (fieldType == typeof(int)) return PlayerPrefs.GetInt(key);
else if (fieldType == typeof(float)) return PlayerPrefs.GetFloat(key);
else if (fieldType == typeof(string)) return PlayerPrefs.GetString(key);
else if (fieldType == typeof(bool)) return PlayerPrefs.GetInt(key) == 1;
return GetDefaultValue(fieldType);
}
// 获取类型的默认值
private object GetDefaultValue(Type type)
{
return type.IsValueType ? Activator.CreateInstance(type) : null;
}
四、使用示例
假设我们有一个玩家数据类:
public class PlayerData
{
public string name;
public int level;
public float hp;
public bool isVip;
}
使用工具类进行数据操作:
// 保存数据
var player = new PlayerData();
player.name = "张三";
player.level = 5;
player.hp = 98.5f;
player.isVip = true;
PlayerPrefsDataMgr.Instance.SaveData(player, "MainPlayer");
PlayerPrefs.Save(); // 手动保存(可选,Unity会自动保存但建议显式调用)
// 读取数据
PlayerData loadedPlayer = (PlayerData)PlayerPrefsDataMgr.Instance.LoadData(typeof(PlayerData), "MainPlayer");
五、Unity 反射基础
1.反射核心组件
// 1. Type - 获取类的所有信息(字段、属性、方法等)
Type type = typeof(MyClass);
// 2. Assembly - 获取程序集,通过程序集获取Type
Assembly assembly = Assembly.GetExecutingAssembly();
// 3. Activator - 快速实例化对象
object instance = Activator.CreateInstance(type);
2.类型兼容性判断
使用 IsAssignableFrom 方法可以判断一个类型是否能容纳另一个类型的对象(通常用于判断继承关系):
// 定义父子类
public class Father { }
public class Son { } // 注意:这里Son并未继承Father,后面会看到效果
void CheckAssignable()
{
Type fatherType = typeof(Father);
Type sonType = typeof(Son);
// 判断Father类型是否能容纳Son类型的对象
if (fatherType.IsAssignableFrom(sonType))
{
print("可以装");
Father f = Activator.CreateInstance(sonType) as Father;
print(f);
}
else
{
print("不能装"); // 本例会输出这个结果,因为Son没有继承Father
}
}
A.IsAssignableFrom(B)表示 "B 类型的实例能否赋值给 A 类型的变量"- 只有当 Son 继承自 Father 时,上述判断才会返回 true
- 常用于泛型约束、插件系统等需要动态判断类型关系的场景
3.泛型类型信息获取
反射可以获取泛型类型的类型参数
void GetGenericTypeInfo()
{
// 列表泛型示例
List<int> list = new List<int>();
Type listType = list.GetType();
// 获取泛型参数类型
Type[] genericTypes = listType.GetGenericArguments();
print(genericTypes[0]); // 输出:System.Int32
// 字典泛型示例
Dictionary<string, float> dic = new Dictionary<string, float>();
Type dicType = dic.GetType();
genericTypes = dicType.GetGenericArguments();
print(genericTypes[0]); // 输出:System.String
print(genericTypes[1]); // 输出:System.Single
}
- 序列化 / 反序列化泛型集合
- 通用数据处理框架
- 动态生成适配不同泛型类型的代码
六、项目示例一:数据管理类的创建
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// PlayerPrefs数据管理类 统一管理数据的存储和读取
/// </summary>
public class PlayerPrefsDataMgr
{
private static PlayerPrefsDataMgr instance = new PlayerPrefsDataMgr();
public static PlayerPrefsDataMgr Instance
{
get
{
return instance;
}
}
private PlayerPrefsDataMgr()
{
}
/// <summary>
/// 存储数据
/// </summary>
/// <param name="data">数据对象</param>
/// <param name="keyName">数据对象的唯一key 自己控制</param>
public void SaveData( object data, string keyName )
{
//就是要通过 Type 得到传入数据对象的所有的 字段
//然后结合 PlayerPrefs来进行存储
}
/// <summary>
/// 读取数据
/// </summary>
/// <param name="type">想要读取数据的 数据类型Type</param>
/// <param name="keyName">数据对象的唯一key 自己控制</param>
/// <returns></returns>
public object LoadData( Type type, string keyName )
{
//不用object对象传入 而使用 Type传入
//主要目的是节约一行代码(在外部)
//假设现在你要 读取一个Player类型的数据 如果是object 你就必须在外部new一个对象传入
//现在有Type的 你只用传入 一个Type typeof(Player) 然后我在内部动态创建一个对象给你返回出来
//达到了 让你在外部 少写一行代码的作用
//根据你传入的类型 和 keyName
//依据你存储数据时 key的拼接规则 来进行数据的获取赋值 返回出去
return null;
}
}
七、项目示例二:结合反射——常用数据类型存储
1.核心代码结构梳理
| 脚本名称 | 核心作用 | 关键功能点 |
|---|---|---|
PlayerPrefsDataMgr | 通用数据管理工具类 | 单例模式、反射存储、多类型适配 |
PlayerInfo | 玩家数据实体类(存储数据的 “载体”) | 定义 int/string/float/bool 字段 |
Test | 测试脚本(触发数据存储的 “入口”) | 实例化玩家数据、调用存储方法 |
2.关键知识点拆解
(1)数据实体类设计:定义要存储的字段
PlayerInfo 类是实际存储数据的 “容器”,定义了游戏中最常用的四种数据类型,字段需用 public 修饰(反射默认只能获取 public 成员)
public class PlayerInfo
{
// int 类型:玩家年龄
public int age = 10;
// string 类型:玩家名称
public string name = "未命名";
// float 类型:玩家身高
public float height = 177.5f;
// bool 类型:玩家性别(true=男,false=女)
public bool sex = true;
}
注意:若需存储私有字段,需在 GetFields() 中添加参数 BindingFlags.NonPublic | BindingFlags.Instance
(2)工具类核心逻辑:反射 + 多类型存储适配
【1】单例模式:确保全局唯一实例
通过私有构造函数 + 静态实例,避免重复创建管理器,保证数据操作的一致性
private static PlayerPrefsDataMgr instance = new PlayerPrefsDataMgr();
public static PlayerPrefsDataMgr Instance { get { return instance; } }
// 私有构造函数:禁止外部 new
private PlayerPrefsDataMgr() { }
【2】反射获取字段:动态遍历数据对象
通过 Type 和 FieldInfo 动态获取 PlayerInfo 的所有字段,无需手动写每个字段的存储逻辑
public void SaveData(object data, string keyName)
{
// 1. 获取数据对象的类型(如 PlayerInfo)
Type dataType = data.GetType();
// 2. 获取该类型的所有 public 字段
FieldInfo[] infos = dataType.GetFields();
// 3. 遍历每个字段,生成唯一键并存储
string saveKeyName = "";
foreach (var info in infos)
{
// 生成唯一键:规则=对象标识_类名_字段类型_字段名
// 示例:Player1_PlayerInfo_Int32_age(Player1是对象标识,区分多个玩家)
saveKeyName = $"{keyName}_{dataType.Name}_{info.FieldType.Name}_{info.Name}";
// 4. 提取字段值,调用存储方法
object fieldValue = info.GetValue(data); // 获取当前字段的值(如 age=10)
SaveValue(fieldValue, saveKeyName);
}
}
键名规则的优势:
- 避免键冲突(如不同类的
age字段,通过 “类名” 区分); - 便于调试(从键名可直接知道字段所属类、类型);
- 支持多实例存储(如 “Player1” 和 “Player2” 的不同数据)。
【3】多类型适配:兼容 PlayerPrefs 不支持的类型
PlayerPrefs 原生仅支持 int/float/string,通过 SaveValue 方法适配 bool 类型(用 1/0 映射)
private void SaveValue(object value, string keyName)
{
Type fieldType = value.GetType(); // 获取字段类型(如 int、bool)
if (fieldType == typeof(int))
{
PlayerPrefs.SetInt(keyName, (int)value);
Debug.Log($"存储int:{keyName} = {value}");
}
else if (fieldType == typeof(float))
{
PlayerPrefs.SetFloat(keyName, (float)value);
Debug.Log($"存储float:{keyName} = {value}");
}
else if (fieldType == typeof(string))
{
PlayerPrefs.SetString(keyName, value.ToString());
Debug.Log($"存储string:{keyName} = {value}");
}
else if (fieldType == typeof(bool))
{
// bool 转 int 存储:true=1,false=0
PlayerPrefs.SetInt(keyName, (bool)value ? 1 : 0);
Debug.Log($"存储bool:{keyName} = {value}(实际存{((bool)value ? 1 : 0)})");
}
// (可选)补充其他类型:如 long(转 string 存储)
else if (fieldType == typeof(long))
{
PlayerPrefs.SetString(keyName, value.ToString());
}
}
3.测试脚本:触发数据存储
Test 脚本在 Start 中实例化 PlayerInfo,并调用工具类存储数据,流程简单直观
public class Test : MonoBehaviour
{
void Start()
{
// 1. 实例化玩家数据(模拟玩家信息)
PlayerInfo player = new PlayerInfo();
// (可选)修改默认值(如玩家改名)
player.name = "张三";
player.age = 18;
// 2. 调用工具类存储数据,keyName=Player1(标识该玩家)
PlayerPrefsDataMgr.Instance.SaveData(player, "Player1");
// (重要)强制保存到硬盘(PlayerPrefs默认延迟保存,显式调用更安全)
PlayerPrefs.Save();
}
}
八、项目示例三:结合反射——List数据类型存储
1.核心功能:List 集合的存储实现
本次代码在原有基础上新增了对 IList 接口(List 实现了该接口)的支持,通过反射判断字段是否为集合类型,并采用 "先存数量,再存元素" 的策略实现存储。
(1)关键代码解析:List 存储逻辑
在 SaveValue 方法中新增对集合类型的判断和处理:
private void SaveValue(object value, string keyName)
{
Type fieldType = value.GetType();
// ... 省略原有 int/float/string/bool 类型处理 ...
// 新增:判断是否为集合类型(实现了 IList 接口)
else if (typeof(IList).IsAssignableFrom(fieldType))
{
Debug.Log("存储List:" + keyName);
// 转换为 IList 接口(统一处理所有集合类型)
IList list = value as IList;
// 第一步:存储集合元素数量(用于读取时确定循环次数)
PlayerPrefs.SetInt(keyName, list.Count);
// 第二步:遍历集合元素,逐个存储
int index = 0;
foreach (object obj in list)
{
// 元素的键名规则:原键名 + 索引(如 Player1_list_0, Player1_list_1)
SaveValue(obj, keyName + "_" + index);
index++;
}
}
}
- 先通过
typeof(IList).IsAssignableFrom(fieldType)判断字段是否为集合(List、ArrayList等); - 存储集合长度(
list.Count),确保读取时知道需要加载多少个元素; - 遍历集合元素,递归调用
SaveValue方法逐个存储,元素键名在原键名后加索引(避免冲突)。
(2)示例:PlayerInfo 中的 List 字段
在 PlayerInfo 类中添加 List<int> 字段
public class PlayerInfo
{
// ... 原有字段 ...
// 新增:整数列表(如玩家背包物品ID)
public List<int> list = new List<int>() { 1, 2, 3, 4 };
}
当调用 SaveData 存储 PlayerInfo 对象时,会自动触发 List 的存储逻辑
// 测试代码
PlayerInfo p = new PlayerInfo();
PlayerPrefsDataMgr.Instance.SaveData(p, "Player1");
九、项目示例:结合反射——Dictionary数据类型存储
1.Dictionary 存储逻辑
(1)Dictionary 存储实现
在 SaveValue 方法中新增字典类型的判断和处理:
private void SaveValue(object value, string keyName)
{
Type fieldType = value.GetType();
// ... 省略原有 int/float/string/bool/List 类型处理 ...
// 新增:判断是否为字典类型(实现了 IDictionary 接口)
else if (typeof(IDictionary).IsAssignableFrom(fieldType))
{
Debug.Log("存储Dictionary:" + keyName);
// 转换为 IDictionary 接口(统一处理所有字典类型)
IDictionary dic = value as IDictionary;
// 第一步:存储字典键值对数量(用于读取时确定循环次数)
PlayerPrefs.SetInt(keyName, dic.Count);
// 第二步:遍历字典,分别存储键(Key)和值(Value)
int index = 0;
foreach (object key in dic.Keys)
{
// 键的存储规则:原键名 + "_key_" + 索引(如 Player1_dic_key_0)
SaveValue(key, $"{keyName}_key_{index}");
// 值的存储规则:原键名 + "_value_" + 索引(如 Player1_dic_value_0)
SaveValue(dic[key], $"{keyName}_value_{index}");
index++;
}
}
}
- 通过
typeof(IDictionary).IsAssignableFrom(fieldType)判断字段是否为字典(Dictionary<TKey, TValue>等); - 存储字典长度(
dic.Count),确保读取时知道需要加载多少组键值对; - 遍历字典的键集合,对每个键值对:
- 键的存储键名:
原键名 + "_key_" + 索引 - 值的存储键名:
原键名 + "_value_" + 索引通过索引关联键和值,保证读取时能正确配对。
- 键的存储键名:
(2)示例:PlayerInfo 中的 Dictionary 字段
在 PlayerInfo 类中添加 Dictionary<int, string> 字段,模拟玩家的物品 ID 与名称映射:
public class PlayerInfo
{
// ... 原有字段(含 List) ...
// 新增:字典字段(键为int类型,值为string类型)
public Dictionary<int, string> dic = new Dictionary<int, string>()
{
{ 1, "生命值药水" },
{ 2, "魔法值药水" }
};
}
调用 SaveData 存储时,字典会被自动处理:
PlayerInfo p = new PlayerInfo();
PlayerPrefsDataMgr.Instance.SaveData(p, "Player1");
十、项目示例:结合反射——自定义类成员存储
1.自定义类 存储逻辑
(1)自定义类 存储实现
在 SaveValue 方法中新增字典类型的判断和处理:
private void SaveValue(object value, string keyName)
{
Type fieldType = value.GetType();
// ... 省略原有 int/float/string/bool/List 类型处理 ...
//基础数据类型都不是 那么可能就是自定义类型
else
{
SaveData(value, keyName);
}
}
(2)示例:PlayerInfo 中的 自定义类 字段
public class ItemInfo
{
public int id;
public int num;
public ItemInfo()
{
}
public ItemInfo(int id, int num)
{
this.id = id;
this.num = num;
}
}
public class PlayerInfo
{
// ... 原有字段(含 List) ...
public ItemInfo itemInfo = new ItemInfo(3,99);
public List<ItemInfo> itemList = new List<ItemInfo>() {
new ItemInfo(1, 10),
new ItemInfo(2, 20),
};
public Dictionary<int, ItemInfo> itemDic = new Dictionary<int, ItemInfo>()
{
{ 3, new ItemInfo(3, 22)},
{ 4, new ItemInfo(4, 33)},
};
}
十一、项目示例:结合反射——读取常用数据类型
1.数据读取入口方法(LoadData)
/// <summary>
/// 读取数据
/// </summary>
/// <param name="type">想要读取数据的 数据类型Type</param>
/// <param name="keyName">数据对象的唯一key 自己控制</param>
/// <returns></returns>
public object LoadData( Type type, string keyName )
{
//不用object对象传入 而使用 Type传入
//主要目的是节约一行代码(在外部)
//假设现在你要 读取一个Player类型的数据 如果是object 你就必须在外部new一个对象传入
//现在有Type的 你只用传入 一个Type typeof(Player) 然后我在内部动态创建一个对象给你返回出来
//达到了 让你在外部 少写一行代码的作用
//根据你传入的类型 和 keyName
//依据你存储数据时 key的拼接规则 来进行数据的获取赋值 返回出去
//根据传入的Type 创建一个对象 用于存储数据
object data = Activator.CreateInstance(type);
//要往这个new出来的对象中存储数据 填充数据
//得到所有字段
FieldInfo[] infos = type.GetFields();
//用于拼接key的字符串
string loadKeyName = "";
//用于存储 单个字段信息的 对象
FieldInfo info;
for (int i = 0; i < infos.Length; i++)
{
info = infos[i];
//key的拼接规则 一定是和存储时一模一样 这样才能找到对应数据
loadKeyName = keyName + "_" + type.Name +
"_" + info.FieldType.Name + "_" + info.Name;
//有key 就可以结合 PlayerPrefs来读取数据
//填充数据到data中
info.SetValue(data, LoadValue(info.FieldType, loadKeyName));
}
return data;
}
核心作用:
- 作为读取数据的入口,接收要读取的数据类型和唯一标识 key
- 使用反射动态创建目标类型的实例(
Activator.CreateInstance(type)) - 获取该类型的所有字段信息(
type.GetFields()) - 按照与存储时完全一致的规则拼接键名(保证数据能正确对应)
- 循环为每个字段赋值(通过
LoadValue方法获取值并设置到对象中)
关键点:
- 反射创建对象实例,无需提前知道具体类型
- 严格遵循与存储时一致的键名规则,确保数据正确匹配
- 字段赋值使用
FieldInfo.SetValue方法,实现动态数据填充
2. 单个字段值读取方法(LoadValue)
/// <summary>
/// 得到单个数据的方法
/// </summary>
/// <param name="fieldType">字段类型 用于判断 用哪个api来读取</param>
/// <param name="keyName">用于获取具体数据</param>
/// <returns></returns>
private object LoadValue(Type fieldType, string keyName)
{
//根据 字段类型 来判断 用哪个API来读取
if( fieldType == typeof(int) )
{
return PlayerPrefs.GetInt(keyName, 0);
}
else if (fieldType == typeof(float))
{
return PlayerPrefs.GetFloat(keyName, 0);
}
else if (fieldType == typeof(string))
{
return PlayerPrefs.GetString(keyName, "");
}
else if (fieldType == typeof(bool))
{
//根据自定义存储bool的规则 来进行值的获取
return PlayerPrefs.GetInt(keyName, 0) == 1 ? true : false;
}
return null;
}
核心作用:
- 根据字段类型,调用对应的 PlayerPrefs 读取方法
- 是处理各种数据类型的分发中心
- 为每种基本类型提供默认值,避免数据不存在时出错
关键技术点:
- 类型判断:严格区分不同数据类型,使用对应的读取方法
- bool 类型特殊处理:因为 PlayerPrefs 不直接支持 bool,所以通过 int 值还原
- 递归处理:对于复杂类型(集合、自定义类)采用递归方式处理
- 扩展性:可以很容易地添加对新数据类型的支持
十二、项目示例:结合反射——读取List数据类型
1.List 集合读取的核心思路
- 先读取 List 的长度(元素数量)
- 根据长度循环读取每个元素
- 对每个元素进行类型判断和递归处理
- 将读取到的元素添加到新创建的 List 中
2.List 集合读取代码解析
(1)List 类型判断
else if (typeof(IList).IsAssignableFrom(fieldType))
这段代码通过判断字段类型是否实现了 IList 接口,来识别所有 List 集合类型。这种方式可以处理任何 List<T>泛型集合
(2)读取 List 长度
// 读取List的长度
int count = PlayerPrefs.GetInt(keyName, 0);
在存储 List 时,我们首先存储了 List 的长度,因此读取时也先获取这个长度值,作为后续循环读取元素的依据。
(3)创建 List 实例
// 创建对应类型的List实例
IList list = Activator.CreateInstance(fieldType) as IList;
使用反射动态创建 List 实例,这里的关键是Activator.CreateInstance(fieldType),它能够根据传递的 List 类型(如 List<int>)创建对应的实例。
(4)获取元素类型
// 获取List中元素的类型
Type elementType = fieldType.GetGenericArguments()[0];
通过GetGenericArguments()方法获取 List 的泛型参数类型,即集合中元素的实际类型。例如,对于 List<int>,这里将返回 int 类型。
(5)循环读取元素
// 循环读取每个元素并添加到List中
for (int i = 0; i < count; i++)
{
object element = LoadValue(elementType, keyName + i);
list.Add(element);
}
根据 List 长度循环读取每个元素:
- 使用之前获取的元素类型作为参数
- 键名规则与存储时一致:
keyName + 索引 - 递归调用 LoadValue 方法,支持元素为复杂类型
- 将读取到的元素添加到 List 中
十三、项目示例:结合反射——读取Dictionary数据类型数据
1.Dictionary 字典读取的核心思路
字典与列表不同,它包含键和值两部分数据,读取时需要同时还原这两部分信息。核心实现思路是:
- 先读取字典中键值对的数量
- 根据数量循环读取每个键值对
- 分别读取键和对应的值
- 将读取到的键值对添加到新创建的字典中
- 支持键和值为任意数据类型(包括自定义类)
2.Dictionary 读取代码解析
(1)字典类型判断
else if (typeof(IDictionary).IsAssignableFrom(fieldType))
这段代码通过判断字段类型是否实现了 IDictionary 接口,来识别所有字典类型。这种方式可以处理任何 Dictionary<TKey, TValue> 泛型字典,包括:
- Dictionary<int, string>
- Dictionary<string, int>
- Dictionary<int, 自定义类>
- 其他实现了 IDictionary 接口的字典类型
(2)读取字典长度
// 读取字典中键值对的数量
int count = PlayerPrefs.GetInt(keyName, 0);
在存储字典时,我们首先存储了字典中键值对的数量,因此读取时也先获取这个数量值,作为后续循环读取键值对的依据。
(3)创建字典实例
// 创建对应类型的字典实例
IDictionary dic = (IDictionary)Activator.CreateInstance(fieldType);
使用反射动态创建字典实例,Activator.CreateInstance(fieldType)能够根据传递的字典类型(如 Dictionary<int, string>)创建对应的实例。
(4)获取键和值的类型
// 获取字典的键类型和值类型
Type[] genericTypes = fieldType.GetGenericArguments();
Type keyType = genericTypes[0];
Type valueType = genericTypes[1];
字典是双泛型类型,通过GetGenericArguments()方法可以获取到两个泛型参数:
- 第一个是键的类型(TKey)
- 第二个是值的类型(TValue)
这一步是实现通用字典读取的关键,确保我们能正确读取不同类型的键和值。
(5)循环读取键值对
// 循环读取每个键值对
for (int i = 0; i < count; i++)
{
// 读取键
object key = LoadValue(keyType, keyName + "_key_" + i);
// 读取值
object value = LoadValue(valueType, keyName + "_value_" + i);
// 添加到字典
dic.Add(key, value);
}
根据字典长度循环读取每个键值对:
- 键的存储规则:
keyName + "_key_" + 索引 - 值的存储规则:
keyName + "_value_" + 索引 - 分别使用键类型和值类型作为参数调用 LoadValue 方法
- 递归处理支持键和值为复杂类型
- 将读取到的键值对添加到字典中
十四、项目示例:结合反射——常用数据类型存储
1.自定义类读取的核心思路
读取自定义类的核心挑战在于如何递归处理类中的所有字段,包括基本类型、集合类型和其他自定义类。实现思路是:
- 使用反射创建自定义类的实例
- 获取类中所有字段的信息
- 根据存储时的键名规则,为每个字段生成对应的读取键
- 递归读取每个字段的值,无论其类型是基本类型、集合还是其他自定义类
- 将读取到的字段值赋值给创建的实例并返回
2.自定义类定义示例
(1)自定义类的识别与处理
在LoadValue方法中,我们通过排除法识别自定义类:
// 处理自定义类 - 递归调用LoadData
else
{
return LoadData(fieldType, keyName);
}
这段代码处理所有不是基本类型、List 或 Dictionary 的类型,这些类型被视为自定义类。通过递归调用LoadData方法,实现了对任意层级嵌套自定义类的支持。
(2)自定义类实例的创建
// 根据类型创建对象实例
object data = Activator.CreateInstance(type);
使用Activator.CreateInstance(type)动态创建自定义类的实例,这要求自定义类必须有无参构造函数,否则会抛出异常。
(3)字段的反射获取与赋值
// 获取所有字段信息
FieldInfo[] infos = type.GetFields();
// ...
info.SetValue(data, LoadValue(info.FieldType, loadKeyName));
通过反射获取自定义类的所有字段信息,并使用FieldInfo.SetValue方法为字段赋值,实现了动态填充对象数据的功能。
(4)键名规则的保持
loadKeyName = keyName + "_" + type.Name +
"_" + info.FieldType.Name + "_" + info.Name;
十五、PlayerPrefs 的加密思路
1.找不到
思想:把存在硬盘上的内容放在一个不容易找到的地方
(1)多层文件夹包裹
(2)名字辨识度低
缺点:但是对于PlayerPrefs不太适用,因为位置已经固定了,我们改不了
2.看不懂
让数据的Key和Value让别人看不懂,俗称加密。
3.解不出
不让别人获取到你加密的规则,就解不出来了
——Unity PlayerPrefs 数据管理工具类详解&spm=1001.2101.3001.5002&articleId=152048700&d=1&t=3&u=b33963e50ee9417a9dd43f5a3a2df93f)
3560

被折叠的 条评论
为什么被折叠?



