目录
一、简介
在上位机开发中,经常会有数据需要长期保存,比如常见的配置文件,即使软件关闭,下次重新打开也不会受到影响,但也有些数据,不适合写到配置文件中,比如软件的操作记录,特殊的一些全局变量等,因此我写了一个简单的本地存储系统,将数据写到 Json 文件中,并让其和缓存中的变量保持一致。
我们需要实现下面的功能:
1.json 文件如果不存在,自动创建
2.对应路径文件夹如果不存在,自动创建
3.软件运行后,json 文件的数据要自动加载到变量中
4.软件中进行了添加,删除,任何数据改变都要同步到 json 文件中
5.保存数据的类型,目前设置为两种,键值对和数组
6.提供大部分场景对应的接口
二、实现效果
新建一个 .Net Framework 4.8 控制台项目,项目名随意,写代码之前,先添加一个插件
添加一个类 LocalStorage.cs
代码:
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
/// <summary>
/// 简单的本地数据读写系统
/// </summary>
internal class LocalStorage
{
#region 字段
private string _jsonPath = string.Empty;
private Dictionary<string, JToken> _data = new Dictionary<string, JToken>();
private readonly object _lock = new object();
#endregion
#region 构造函数
/// <summary>
/// 构造函数
/// </summary>
/// <param name="path">路径</param>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ArgumentException"></exception>
public LocalStorage(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));
string suffix = Path.GetExtension(path);
if (string.IsNullOrEmpty(suffix))
throw new ArgumentException("路径必须包含文件名和扩展名", nameof(path));
_jsonPath = path;
InitializeStorage();
}
#endregion
#region 读/写 键值对
/// <summary>
/// 设置键值对
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key">key</param>
/// <param name="value">值</param>
public void SetValue<T>(string key, T value) where T : struct, IConvertible
{
lock (_lock)
{
_data[key] = JToken.FromObject(value);
SaveToLocal();
}
}
/// <summary>
/// 设置键值对
/// </summary>
/// <param name="key">key</param>
/// <param name="value">值</param>
public void SetValue(string key, string value)
{
lock (_lock)
{
_data[key] = JToken.FromObject(value);
SaveToLocal();
}
}
/// <summary>
/// 获取键值对
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key">key</param>
/// <param name="defaultValue">读取失败的默认值</param>
/// <returns></returns>
public T GetValue<T>(string key, T defaultValue = default)
{
return _data.TryGetValue(key, out var token) ? token.ToObject<T>() : defaultValue;
}
/// <summary>
/// 获取键值对
/// </summary>
/// <param name="key">key</param>
/// <param name="type">反射类型</param>
/// <returns></returns>
public object GetValue(string key, Type type)
{
if (_data.ContainsKey(key))
return _data[key].ToObject(type);
return null;
}
/// <summary>
/// 获取键值对
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key">key</param>
/// <param name="value">值</param>
/// <returns></returns>
public bool TryGetValue<T>(string key, out T value)
{
if (_data.TryGetValue(key, out var token))
{
value = token.ToObject<T>();
return true;
}
value = default;
return false;
}
#endregion
#region 读/写 数组
/// <summary>
/// 设置列表
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key">key</param>
/// <param name="list">列表</param>
public void SetList<T>(string key, List<T> list) where T : struct, IConvertible
{
lock (_lock)
{
_data[key] = JToken.FromObject(list);
SaveToLocal();
}
}
/// <summary>
/// 设置列表
/// </summary>
/// <param name="key">key</param>
/// <param name="list">列表</param>
public void SetList(string key, List<string> list)
{
lock (_lock)
{
_data[key] = JToken.FromObject(list);
SaveToLocal();
}
}
/// <summary>
/// 向列表中添加数据
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <param name="value"></param>
public void ListAdd<T>(string key, T value) where T : struct, IConvertible
{
lock (_lock)
{
if (_data.TryGetValue(key, out var token))
{
if (token is JArray array)
{
JToken jToken = JToken.FromObject(value);
if (!array.Any(item => JToken.DeepEquals(item, jToken)))
array.Add(jToken);
}
}
else
{
var newArray = new JArray { JToken.FromObject(value) };
_data[key] = newArray;
}
SaveToLocal();
}
}
/// <summary>
/// 向列表中添加数据
/// </summary>
/// <param name="key">key</param>
/// <param name="value">要添加的元素</param>
public void ListAdd(string key, string value)
{
lock (_lock)
{
if (_data.TryGetValue(key, out var token))
{
if (token is JArray array)
{
JToken jToken = JToken.FromObject(value);
if (!array.Any(item => JToken.DeepEquals(item, jToken)))
array.Add(jToken);
}
}
else
{
var newArray = new JArray { JToken.FromObject(value) };
_data[key] = newArray;
}
SaveToLocal();
}
}
/// <summary>
/// 获取列表
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key">key</param>
/// <param name="defaultValue">读取失败的默认值</param>
/// <returns></returns>
public List<T> GetList<T>(string key, List<T> defaultValue = null)
{
if (_data.TryGetValue(key, out var token) && token != null)
{
var result = token.ToObject<List<T>>();
return result != null ? result : defaultValue;
}
return defaultValue;
}
/// <summary>
/// 获取列表
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <param name="value"></param>
/// <returns></returns>
public bool TryGetList<T>(string key, out T value)
{
if (_data.TryGetValue(key, out var token))
{
value = token.ToObject<T>();
return true;
}
value = default;
return false;
}
#endregion
#region 删除
/// <summary>
/// 删除指定key
/// </summary>
/// <param name="key">key</param>
public void Remove(string key)
{
lock (_lock)
{
if (_data.Remove(key))
SaveToLocal();
}
}
/// <summary>
/// 删除指定key一定范围内的元素
/// </summary>
/// <param name="key">key</param>
/// <param name="start">开始的下标</param>
/// <param name="count">删除的个数</param>
public void Remove(string key, int start, int count)
{
lock (_lock)
{
if (_data.TryGetValue(key, out var token))
{
if (token is JArray jArray)
{
if (start < 0) start = 0;
if (start >= jArray.Count) return;
if (count < 0) count = 0;
if (start + count > jArray.Count)
count = jArray.Count - start;
for (int i = 0; i < count; i++)
{
jArray.RemoveAt(start);
}
if (jArray.Count == 0)
_data.Remove(key);
SaveToLocal();
}
}
}
}
/// <summary>
/// 删除指定key指定的元素
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <param name="value"></param>
public void Remove<T>(string key, T value) where T : struct, IConvertible
{
lock (_lock)
{
if (_data.TryGetValue(key, out var token))
{
if (token is JArray jArray)
{
var itemToRemove = jArray.FirstOrDefault(item => item.Value<T>().Equals(value));
if (itemToRemove != null)
{
jArray.Remove(itemToRemove);
// 如果数组为空,可以选择删除整个键
if (!jArray.Any())
_data.Remove(key);
SaveToLocal();
}
}
}
}
}
/// <summary>
/// 删除指定key指定的元素
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <param name="value"></param>
public void Remove(string key, string value)
{
lock (_lock)
{
if (_data.TryGetValue(key, out var token))
{
if (token is JArray jArray)
{
var itemToRemove = jArray.FirstOrDefault(item => item.Type == JTokenType.String && item.Value<string>() == value);
if (itemToRemove != null)
{
jArray.Remove(itemToRemove);
if (!jArray.Any())
_data.Remove(key);
SaveToLocal();
}
}
}
}
}
/// <summary>
/// 删除指定key元素对应的下标
/// </summary>
/// <param name="key"></param>
/// <param name="index"></param>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public void RemoveAt(string key, int index)
{
lock (_lock)
{
if (_data.TryGetValue(key, out var token))
{
if (token is JArray jArray)
{
if (index >= 0 && index < jArray.Count)
{
jArray.RemoveAt(index);
// 如果数组为空,可以选择删除整个键
if (!jArray.Any())
_data.Remove(key);
SaveToLocal();
}
else
{
throw new ArgumentOutOfRangeException(nameof(index), "Index is out of range.");
}
}
}
}
}
/// <summary>
/// 删除指定键值对包含在参数list中的元素
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key">key</param>
/// <param name="list">要删除的列表</param>
public void Remove<T>(string key, List<T> list) where T : struct, IConvertible
{
lock (_lock)
{
if (list == null || list.Count == 0) return;
if (_data.TryGetValue(key, out JToken token))
{
if (token is JArray jArray)
{
var itemsToRemove = jArray.Where(item => list.Contains(item.ToObject<T>())).ToList();
foreach (var item in itemsToRemove)
{
jArray.Remove(item);
}
if (jArray.Count == 0)
_data.Remove(key);
SaveToLocal();
}
}
}
}
/// <summary>
/// 删除指定键值对包含在参数list中的元素
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key">key</param>
/// <param name="list">要删除的列表</param>
public void Remove(string key, List<string> list)
{
lock (_lock)
{
if (list == null || list.Count == 0) return;
if (_data.TryGetValue(key, out JToken token))
{
if (token is JArray jArray)
{
var itemsToRemove = jArray.Where(item => list.Contains(item.ToObject<string>())).ToList();
foreach (var item in itemsToRemove)
{
jArray.Remove(item);
}
if (jArray.Count == 0)
_data.Remove(key);
SaveToLocal();
}
}
}
}
#endregion
#region 其他
private void SaveToLocal()
{
EnsureFileExists();
File.WriteAllText(_jsonPath, JsonConvert.SerializeObject(_data, Formatting.Indented));
}
private void EnsureFileExists()
{
if (!File.Exists(_jsonPath))
{
string directPath = Path.GetDirectoryName(_jsonPath);
if (!Directory.Exists(directPath))
Directory.CreateDirectory(directPath);
File.Create(_jsonPath).Close();
}
}
private void InitializeStorage()
{
EnsureFileExists();
try
{
string json = File.ReadAllText(_jsonPath);
if (!string.IsNullOrEmpty(json))
_data = JsonConvert.DeserializeObject<Dictionary<string, JToken>>(json);
}
catch (Exception ex)
{
Console.WriteLine("读取文件错误:{0}", ex.Message);
}
}
#endregion
}
三、接口的调用
1.添加键值对
SetValue 这个方法我添加了泛型约束, T 只能是值类型,所以下面我又添加了一个 string 类型的重载方法,这里主要的目的是,既然是简单的存储系统,就尽量不要存储那些比较复杂的数据结构。
namespace LocalSave
{
internal class Program
{
static void Main(string[] args)
{
string path = $"{Environment.CurrentDirectory}\\Config\\LocalStorage.json";
LocalStorage localStorage = new LocalStorage(path);
localStorage.SetValue("myName", "张三");
Console.WriteLine("\n执行完成");
Console.ReadKey();
}
}
}
运行后,自动生成了文件,并写入了 json
2.读取键值对
读取键值对,这里有几个重载方法
代码:
namespace LocalSave
{
internal class Program
{
static void Main(string[] args)
{
string path = $"{Environment.CurrentDirectory}\\Config\\LocalStorage.json";
LocalStorage localStorage = new LocalStorage(path);
string name = localStorage.GetValue<string>("myName");
Console.WriteLine("name:{0}", name);
Console.WriteLine("\n执行完成");
Console.ReadKey();
}
}
}
运行:
也可以用反射指定 T 的类型
namespace LocalSave
{
internal class Program
{
static void Main(string[] args)
{
string path = $"{Environment.CurrentDirectory}\\Config\\LocalStorage.json";
LocalStorage localStorage = new LocalStorage(path);
var name = localStorage.GetValue("myName", typeof(string));
Console.WriteLine("name:{0}", name);
Console.WriteLine("\n执行完成");
Console.ReadKey();
}
}
}
还可以使用 TryGetValue 方法,不必指定类,如果 out 变量的类型对应不上,则会报错。
代码:
namespace LocalSave
{
internal class Program
{
static void Main(string[] args)
{
string path = $"{Environment.CurrentDirectory}\\Config\\LocalStorage.json";
LocalStorage localStorage = new LocalStorage(path);
string name;
bool isRes = localStorage.TryGetValue("myName", out name);
Console.WriteLine("isRes:{0},name:{1}", isRes, name);
Console.WriteLine("\n执行完成");
Console.ReadKey();
}
}
}
运行:
3.添加数组
SetList 方法这里依然是采用两个重载方法,用泛型约束限制了 T 的类型只能是值类型和 string 类型,一般来说,使用 List 用来存储数组类型已经完全够用了,原来还写了 SetArray 方法,后面删掉了。
代码:
namespace LocalSave
{
internal class Program
{
static void Main(string[] args)
{
string path = $"{Environment.CurrentDirectory}\\Config\\LocalStorage.json";
LocalStorage localStorage = new LocalStorage(path);
List<string> list1 = new List<string>() { "a", "b", "c", "d" };
List<int> list2 = new List<int>() { 1, 34, 5, 23, 56, 64 };
localStorage.SetList("list1", list1);
localStorage.SetList("list2", list2);
Console.WriteLine("\n执行完成");
Console.ReadKey();
}
}
}
运行:
4.读取数组
读取数组也有几个重载,建议看下对应的接口
代码:
namespace LocalSave
{
internal class Program
{
static void Main(string[] args)
{
string path = $"{Environment.CurrentDirectory}\\Config\\LocalStorage.json";
LocalStorage localStorage = new LocalStorage(path);
List<string> list1 = localStorage.GetList<string>("list1");
Console.WriteLine(string.Join(",", list1));
List<int> list2 = localStorage.GetList<int>("list2");
Console.WriteLine(string.Join(",", list2));
Console.WriteLine("\n执行完成");
Console.ReadKey();
}
}
}
运行:
另一种读取方式,不需要指定接收值的类型
namespace LocalSave
{
internal class Program
{
static void Main(string[] args)
{
string path = $"{Environment.CurrentDirectory}\\Config\\LocalStorage.json";
LocalStorage localStorage = new LocalStorage(path);
List<string> list;
bool isRes = localStorage.TryGetList("list1", out list);
Console.WriteLine("isRes:{0},list:{1}", isRes, string.Join(",", list));
Console.WriteLine("\n执行完成");
Console.ReadKey();
}
}
}
运行:
5.数组追加数据
在 SetList 方法中,数组只能替换,不能向数组追加数据,所以后面我又添加了一个方法 ListAdd,用来向指定 key 内的数组添加数据,如果当前的 key 不存在,那么就自动创建一个键值对,value类型为数组。
代码:
namespace LocalSave
{
internal class Program
{
static void Main(string[] args)
{
string path = $"{Environment.CurrentDirectory}\\Config\\LocalStorage.json";
LocalStorage localStorage = new LocalStorage(path);
localStorage.ListAdd("list2", 3);
Console.WriteLine("\n执行完成");
Console.ReadKey();
}
}
}
运行:
如果 key 不存在,会自动创建一个新的键值对,比如:
代码:
namespace LocalSave
{
internal class Program
{
static void Main(string[] args)
{
string path = $"{Environment.CurrentDirectory}\\Config\\LocalStorage.json";
LocalStorage localStorage = new LocalStorage(path);
localStorage.ListAdd("age", 4);
Console.WriteLine("\n执行完成");
Console.ReadKey();
}
}
}
运行:
6.删除指定的key
会删除整个键值对
代码:
namespace LocalSave
{
internal class Program
{
static void Main(string[] args)
{
string path = $"{Environment.CurrentDirectory}\\Config\\LocalStorage.json";
LocalStorage localStorage = new LocalStorage(path);
localStorage.Remove("age");
Console.WriteLine("\n执行完成");
Console.ReadKey();
}
}
}
运行:
7.删除指定key中指定的元素
这里有两个重载,可以根据对应的需求来使用
1.删除指定 key 指定起点开始指定个数的数组
代码:
namespace LocalSave
{
internal class Program
{
static void Main(string[] args)
{
string path = $"{Environment.CurrentDirectory}\\Config\\LocalStorage.json";
LocalStorage localStorage = new LocalStorage(path);
localStorage.Remove("list1", 1, 3);
Console.WriteLine("\n执行完成");
Console.ReadKey();
}
}
}
运行:
2.删除指定 key 中对应的元素
代码:
namespace LocalSave
{
internal class Program
{
static void Main(string[] args)
{
string path = $"{Environment.CurrentDirectory}\\Config\\LocalStorage.json";
LocalStorage localStorage = new LocalStorage(path);
localStorage.Remove("list2", new List<int> { 1, 34, 5, 23, 56 });
Console.WriteLine("\n执行完成");
Console.ReadKey();
}
}
}
运行:
结束
如果这个帖子对你有用,欢迎 关注 + 点赞 + 留言,谢谢
end