2025Unity 游戏项目开发必备知识点(2)——Unity PlayerPrefs 数据管理工具类详解

目录

一、PlayerPrefs 

1.本质

2.存储位置

3.支持类型

4.常用 API

二、PlayerPrefsDataMgr工具类 核心原理

1.单例模式设计

2.智能键名生成规则

(1)格式

(2)命名规则的优势

3.数据存储实现

三、数据读取功能实现

四、使用示例

五、Unity 反射基础

1.反射核心组件

2.类型兼容性判断

3.泛型类型信息获取

六、项目示例一:数据管理类的创建

七、项目示例二:结合反射——常用数据类型存储

1.核心代码结构梳理

2.关键知识点拆解

(1)数据实体类设计:定义要存储的字段

(2)工具类核心逻辑:反射 + 多类型存储适配

3.测试脚本:触发数据存储

八、项目示例三:结合反射——List数据类型存储

1.核心功能:List 集合的存储实现

(1)关键代码解析:List 存储逻辑

(2)示例:PlayerInfo 中的 List 字段

九、项目示例:结合反射——Dictionary数据类型存储

1.Dictionary 存储逻辑

(1)Dictionary 存储实现

(2)示例:PlayerInfo 中的 Dictionary 字段

十、项目示例:结合反射——自定义类成员存储

 1.自定义类 存储逻辑

(1)自定义类 存储实现

(2)示例:PlayerInfo 中的 自定义类 字段

十一、项目示例:结合反射——读取常用数据类型

1.数据读取入口方法(LoadData)

2. 单个字段值读取方法(LoadValue)

十二、项目示例:结合反射——读取List数据类型

1.List 集合读取的核心思路

2.List 集合读取代码解析

(1)List 类型判断

(2)读取 List 长度

(3)创建 List 实例

(4)获取元素类型

(5)循环读取元素

十三、项目示例:结合反射——读取Dictionary数据类型数据

1.Dictionary 字典读取的核心思路

2.Dictionary 读取代码解析

(1)字典类型判断

(2)读取字典长度

(3)创建字典实例

(4)获取键和值的类型

(5)循环读取键值对

十四、项目示例:结合反射——常用数据类型存储

1.自定义类读取的核心思路

2.自定义类定义示例

(1)自定义类的识别与处理

(2)自定义类实例的创建

(3)字段的反射获取与赋值

(4)键名规则的保持

十五、PlayerPrefs 的加密思路

1.找不到

2.看不懂

3.解不出


一、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++;
        }
    }
}
  1. 先通过 typeof(IList).IsAssignableFrom(fieldType) 判断字段是否为集合(ListArrayList 等)
  2. 存储集合长度list.Count,确保读取时知道需要加载多少个元素;
  3. 遍历集合元素,递归调用 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++;
        }
    }
}
  1. 通过 typeof(IDictionary).IsAssignableFrom(fieldType) 判断字段是否为字典(Dictionary<TKey, TValue> 等);
  2. 存储字典长度(dic.Count),确保读取时知道需要加载多少组键值对;
  3. 遍历字典的键集合,对每个键值对:
    • 键的存储键名:原键名 + "_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 集合读取的核心思路

  1. 先读取 List 的长度(元素数量)
  2. 根据长度循环读取每个元素
  3. 对每个元素进行类型判断和递归处理
  4. 将读取到的元素添加到新创建的 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 字典读取的核心思路

        字典与列表不同,它包含键和值两部分数据,读取时需要同时还原这两部分信息。核心实现思路是:

  1. 先读取字典中键值对的数量
  2. 根据数量循环读取每个键值对
  3. 分别读取键和对应的值
  4. 将读取到的键值对添加到新创建的字典中
  5. 支持键和值为任意数据类型(包括自定义类)

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.自定义类读取的核心思路

        读取自定义类的核心挑战在于如何递归处理类中的所有字段,包括基本类型、集合类型和其他自定义类。实现思路是:

  1. 使用反射创建自定义类的实例
  2. 获取类中所有字段的信息
  3. 根据存储时的键名规则,为每个字段生成对应的读取键
  4. 递归读取每个字段的值,无论其类型是基本类型、集合还是其他自定义类
  5. 将读取到的字段值赋值给创建的实例并返回

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.解不出

        不让别人获取到你加密的规则,就解不出来了

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值