温兴胜 發表於 2024-5-23 13:22:00

IDocList/IDocDict JSON for Delphi and FPC

<h1 id="idoclistidocdict-json-for-delphi-and-fpc">IDocList/IDocDict JSON for Delphi and FPC</h1>
<p>【英文原文】</p>
<p>多年来,我们的开源 <em>mORMot</em> 框架提供了多种方法来处理在运行时定义的数组/对象文档的任意组合,例如通过 JSON,具有许多功能和非常高的性能。</p>
<p><img src="https://img2023.cnblogs.com/blog/370256/202405/370256-20240523133108522-1135628385.png" alt="img" loading="lazy"></p>
<p>我们的 <code>TDocVariant</code>自定义变体类型是处理这类无模式数据的一种强大方式,但一些用户觉得它有些令人困惑。</p>
<p>因此,我们围绕它开发了一套新的接口定义,以简化其使用,同时不牺牲其功能。我们围绕Python列表和字典对它们进行了建模,这已被证明是可行的——当然,也做了一些扩展。</p>
<h3 id="tdocvariant的优缺点">TDocVariant的优缺点</h3>
<p>多年来,我们的 <code>TDocVariant</code>可以存储任何基于JSON/BSON的文档内容,即:</p>
<ul>
<li>面向对象文档的名/值对——在内部被标识为 <code>dvObject</code>子类型;</li>
<li>面向数组文档的值数组(包括嵌套文档)——在内部被标识为 <code>dvArray</code>子类型;</li>
<li>通过嵌套 <code>TDocVariant</code>实例,可以实现上述两者的任意组合。</li>
</ul>
<p>每个 <code>TDocVariant</code>实例也是一个自定义的变体类型:</p>
<ul>
<li>因此,您可以将它存储或转换为变体变量;</li>
<li>您可以使用后期绑定来访问其对象属性,这在现代Pascal的严格世界中有点像魔术;</li>
<li>Delphi IDE(和Lazarus 3.x)调试器对其有原生支持,因此可以将变体内容显示为JSON;</li>
<li>如果您在任何类或记录中定义了变体类型,我们的框架将识别 <code>TDocVariant</code>内容,并将其序列化和反序列化为JSON,例如在其ORM、SOA或Mustache/MVC部分中。</li>
</ul>
<p>这种强大功能也带来了一些缺点:</p>
<ul>
<li>在变体和其 <code>TDocVariantData</code>记录之间切换可能很棘手,有时需要一些令人困惑的指针引用;</li>
<li>每个 <code>TDocVariant</code>实例都可以用作对其他数据的弱引用,或者维护其自身的内容——在某些极端情况下,不正确的使用可能会导致内存泄漏或GPF问题;</li>
<li><code>TDocVariant</code>可以是对象/字典或数组/列表,因此找到正确的方法可能很困难,或者在运行时引发异常;</li>
<li>它从一个简单的存储发展成了一个完整的内存引擎,因此高级功能通常被低估;</li>
<li><code>TDocVariantData</code>记录与大多数Delphi/FPC用户所习惯的类系统相去甚远;</li>
<li>默认情况下,不解析双精度值——只解析货币值——如果你不想损失任何精度,这是有意义的,但也被发现会造成混淆。</li>
</ul>
<p>抱怨够了。</p>
<p>我们只需让它变得更好。<br>
引入IDocList和IDocDict接口</p>
<p>我们引入了两个高级封装接口类型:</p>
<ul>
<li>IDocList(或其别名IDocArray)用于存储元素列表;</li>
<li>IDocDict(或其别名IDocObject)用于存储键值对字典。</li>
</ul>
<p>接口方法和命名遵循通常的Python列表和字典,并在安全且专用于类的IDocList和IDocDict类型中封装它们自己的TDocVariant存储。</p>
<p>您可能会在现代Delphi中这样写:</p>
<pre><code class="language-pascal">var
list: IDocList;
dict: IDocDict;
v: variant;
i: integer;
begin
// 从项目创建一个新的列表/数组
list := DocList(); // 默认情况下允许双精度值

// 遍历列表
for v in list do
    Listbox1.Items.Add(v); // 将变量转换为字符串

// 或列表的一个子范围(使用类似Python的负索引)
for i in list.Range(0, -3) do
    Listbox2.Items.Add(IntToStr(i)); // 作为整数

// 搜索某些元素的存在
assert(list.Exists(2));
assert(list.Exists('four'));

// 从JSON中获取一个对象列表,其中包含一个入侵者
list := DocList('[{"a":0,"b":20},{"a":1,"b":21},"to be ignored",{"a":2,"b":22}]');

// 枚举所有对象/字典,忽略非对象元素
for dict in list.Objects do
begin
    if dict.Exists('b') then
      ListBox2.Items.Add(dict['b']);
    if dict.Get('a', i) then
      ListBox3.Items.Add(IntToStr(i));
end;

// 删除一个元素
list.Del(1);
assert(list.Json = '[{"a":0,"b":20},"to be ignored",{"a":2,"b":22}]');

// 提取一个元素
if list.PopItem(v, 1) then
    assert(v = 'to be ignored');

// 转换为JSON字符串
Label1.Caption := list.ToString;
// 显示 '[{"a":0,"b":20},{"a":2,"b":22}]'
end;
</code></pre>
<p>以及更多高级功能,如排序、搜索和表达式过滤:</p>
<pre><code class="language-pascal">var
v: variant;
f: TDocDictFields;
list, list2: IDocList;
dict: IDocDict;
begin
list := DocList('[{"a":10,"b":20},{"a":1,"b":21},{"a":11,"b":20}]');

// 根据嵌套对象的字段对列表/数组进行排序
list.SortByKeyValue(['b', 'a']);
assert(list.Json = '[{"a":10,"b":20},{"a":11,"b":20},{"a":1,"b":21}]');

// 使用条件表达式枚举列表/数组 :)
for dict in list.Objects('b&lt;21') do
    assert(dict.I['b'] &lt; 21);

// 使用变量作为条件表达式的另一个枚举
for dict in list.Objects('a=', 10) do
    assert(dict.I['a'] = 10);

// 根据条件表达式创建新的IDocList
list2 := list.Filter('b =', 20);
assert(list2.Json = '[{"a":10,"b":20},{"a":11,"b":20}]');

// 直接访问内部TDocVariantData存储
assert(list.Value^.Count = 3);
assert(list.Value^.Kind = dvArray);
assert(dict.Value^.Kind = dvObject);

// 通过变量中介获取TDocVariantData
v := list.AsVariant;
assert(_Safe(v)^.Count = 3);
v := dict.AsVariant;
assert(_Safe(v)^.Count = 2);

// 类似Python的高级方法
if list.Len &gt; 0 then
    while list.PopItem(v) do
    begin
      assert(list.Count(v) = 0); // 计算出现的次数
      assert(not list.Exists(v));
      Listbox1.Items.Add(v.a); // 后期绑定
      dict := DocDictFrom(v); // 从变量转换为IDocDict
      assert(dict.Exists('a') and dict.Exists('b'));
      // 枚举此字典的键值元素
      for f in dict do
      begin
      Listbox2.Items.Add(f.Key);
      Listbox3.Items.Add(f.Value);
      end;
    end;

// 从任何复杂的“紧凑”JSON创建
// (注意键名没有被“引用”)
list := DocList('[{ab:1,cd:{ef:"two"}}]');

// 我们仍然有后期绑定的魔法在工作
assert(list.ab = 1);
assert(list.cd.ef = 'two');

// 从代码中提供的键值对创建字典
dict := DocDict(['one', 1, 'two', 2, 'three', _Arr()]);
assert(dict.Len = 3); // 一个包含3个元素的字典
assert(dict.Json = '{"one":1,"two":2,"three":}');

// 转换为带有美观格式(换行符和空格)的JSON
Memo1.Caption := dic.ToString(jsonHumanReadable);

// 按键名排序
dict.Sort;
assert(dict.Json = '{"one":1,"three":,"two":2}');

// 注意,它将在排序后确保更快的O(log(n))键查找:
// (对于具有大量键的对象,这有利于提高性能)
assert(dict['two'] = 2); // 作为变量值的默认查找
assert(dict.I['two'] = 2); // 显式转换为整数
end;
</code></pre>
<p>以下是 <code>TTextWriter.AddJsonReformat()</code>方法及其 <code>JsonBufferReformat()</code>和 <code>JsonReformat()</code>封装的可用JSON格式:</p>
<ul>
<li><code>jsonCompact</code>是默认的、对机器友好的单行布局</li>
<li><code>jsonHumanReadable</code>会添加换行符和缩进,以获得更人性化的结果</li>
<li><code>jsonUnquotedPropName</code>将生成 <code>jsonHumanReadable</code>布局,但只在必要时才引用所有属性名称:此格式可用于配置文件等场合 - 此格式与MongoDB扩展语法中使用的格式类似,与JSON不兼容:不要与AJAX客户端等一起使用,但它会被我们的所有单元按预期处理为有效的JSON输入,而无需事先校正</li>
<li><code>jsonUnquotedPropNameCompact</code>将生成单行布局,其中包含未引用的属性名称,这是mORMot实例中数据输出量最小的方式</li>
<li>默认情况下,我们依赖于UTF-8编码(在RFC 8259中是必需的),但您可以使用 <code>jsonEscapeUnicode</code>生成纯7位ASCII输出,其中非ASCII字符使用\u####转义,例如默认的python json.dumps</li>
<li><code>jsonNoEscapeUnicode</code>会搜索任何\u####模式,并生成纯UTF-8输出</li>
<li>这些特性不是在此单元中实现,而是在mormot.core.json中实现</li>
</ul>
<p>由于高级实例是接口,并且内部内容是变量,因此它们的使用寿命都是安全和正常的——您不需要编写任何try..finaly list.Free代码。</p>
<p>而且性能仍然很高,因为例如一个巨大的JSON数组会分配一个单独的IDocList,所有嵌套的节点都将作为变体的有效动态数组来保存。</p>
<p>最后两行代码可能展示了我们的mORMot库在Delphi和FPC的JSON库森林/丛林中是如何独树一帜的:</p>
<p><code>assert(DocList('[{ab:1,cd:{ef:"two"}}]').cd.ef = 'two');</code></p>
<p><code>assert(DocList('[{ab:1,cd:{ef:"two"}}]').First('ab&lt;&gt;0').cd.ef = 'two');</code></p>
<p>如果你与标准的Delphi JSON库的工作方式进行比较,以及它与每个节点的类的工作方式,你可能会发现很大的不同!</p>
<p>请注意,这两行代码都可以用古老的Delphi 7编译器进行编译和运行——谁说Pascal语言在当年没有表现力?</p>
<p>我们希望我们成功地开辟了一种与JSON文档交互的新方式,这样你就可以在你的Delphi或FPC项目中使用它。</p>
<p>一如既往,我们欢迎在我们的论坛中提供任何反馈!</p>
<p>顺便说一句,你知道我为什么在代码中选择1.0594631这个数字吗?</p>
<p>提示:这是我在小时候使用Z80 CPU编程音乐时用过的东西……我仍然记得这个常数。😄</p><br><br>
来源:https://www.cnblogs.com/hieroly/p/18208182
頁: [1]
查看完整版本: IDocList/IDocDict JSON for Delphi and FPC