写点什么

动态数据类型

作者:Miracle
  • 2025-09-23
    四川
  • 本文字数:3376 字

    阅读完需:约 11 分钟

动态数据类型
为什么选择动态数据类型


严格来说,其实没有什么弱类型语言和强类型语言的区别。因为所有的语言归根到底,理论上都是有类型的。

程序代码到最后会翻译成机器码,那就是那几种最基础。数据类型。比如说用 rust 的观点来看 u8 i8 i16 u64,I32,float32,float64。最多再加上也用的特别多的那个。降低精度换取存储量的 F16。

语言和语言的区别其实在类型方面的区别其实只有两个区别,

一个是静态类型和动态类型,静态类型就是说在编译前就是在写代码的时候类型一定要确定。

然后呢为了实现这个类型动态化,就比如像 rust 和 c++,包括 JAVA。这些需要确定类型的语言都会引入了各种比如 c++模板 template。GO 的模板啊,包括 JAVA 的。

rust 会引入一个 trait。这就是所谓的泛型编程的概念,

其实本质上是为了让那个类型能够不影响到算法。算法本身其实是不需要或者说是类型通用的。

其实是不 care 类型的。

用同样的方法可以可以适用于不同的类型,比如适用于 16 位整数,32 位整数,64 位整数,甚至适用于比如说排序算法吧。他就会适用于任何支持的比较和交换,比如像快速排序,堆排序。只要任何任何支持的这个比较和交换的这样一个数据集合,这个那么这个所谓的堆排序或者快速排序算法都能够适用。

所以其实这种适用性,用动态类型语言也能达到同样的目的。比如说我们在描述算法的时候不带上类型。然后在运行的时候或者是在最终编译成字节码的时候,再特化所谓的或者叫 freeze 固化,固化为一个类型,其实也是一样的。

但是为什么现在编语言会使用那个模板或 trait 的概念呢?是因为这样的话。语言层面更严谨,然后编检查做的更好。当然对于学习或者是对于大量的非特别专业的这开发人员来说,其实动态类型可能是一个更好的 Idea


所以我们现在需要一个在静态语言里面能够支持的这个动态类型。因为常见的那个动态类型除了基本类型,我还有容器类型,就是我们要需要需要处理。比如说最基础的两种。List

Vector。其实都一样。都是代表一段连续的同样类型。另外一种叫做 dictionary 。或者是 map 或者 Object 都是 kv 就是 kv 结构,这样可以呃准确的表达我们现实。绝大多数概念 object 对象和属性。

那么综合刚才要求,所以我们最早的 dynamic 的版本在 rust 文件的结构是这样子。

pub enum Dynamic {     #[default]    Null,    Bool(bool),    Int(i64),    Float(f32),                   //默认浮点类型    String(SmolStr),    List(Vec<Dynamic>),    Object(BTreeMap<SmolStr, Dynamic>),}
复制代码

这个数据结构的大小呢大概是 40 个字。应该是可以适用于大量的脚本和编译。大量 Rust 的脚本语言 包括 Rhai 或者 Rune 都有类似的结构。

但是呢它有一个比较大的问题是什么?就是说嗯,他没有办法表示。同类型的对象。比如说像嗯浮点数的输出整数的数组。他都要用 dynamic 的表。这样的话呢就这个小的固定类型的数组。会非常难以表达起来会非常复杂。又占空间,而且又慢。


我们就有了下面的一个改进版本就是把小的 List 单独拿出来

pub enum Dynamic {     #[default]    Null,    Bool(bool),    Int(i64),    Float(f32),                   //默认浮点类型    String(SmolStr),    List(Vec<Dynamic>),    VecInt(Vec<i64>),    VecFloat(Vec<f64>),    Object(BTreeMap<SmolStr, Dynamic>),}
复制代码


但是这个版本有个很大的问题。就是他用那个 float64 统一表示所有的 浮点数 和 int64

表示整数, 但是在实际的使用过程中,我们实际上是明确需要知道这个。对的,对的,就是说我一个 32 一个 10 这个字出拿出来。我可以去呃认为它是一个动态的平台,可是所有的整数但是呢, 但是我在存储或者是我内部存储和底层运算的时候,我一定要明确知道。他到底是一个什么样的?类型 i8 i16 还是什么

pub enum Dynamic {     #[default]    Null,    Bool(bool),    U8(u8),    I8(i8),    U16(u16),    I16(i16),    U32(u32),    I32(i32),                   //默认整数类型    U64(u64),    I64(i64),    F32(f32),                   //默认浮点类型    F64(f64),    String(SmolStr),    List(Vec<Dynamic>),    VecInt(Vec<i64>),    VecFloat(Vec<f64>),    Object(BTreeMap<SmolStr, Dynamic>),}
复制代码

这样的话呃这个结构就能够适用于绝大多数的动态语言和静态语言就是比如说我们的一个脚本和我们的那个实现。中间可以用来做通用的数据,类型的数据容器来装东西。但是这里面还有一个问题,就是说呃我们看到我们这里面。一个 但我们现在已经对单独的整数和浮点数的类型的精确的控制。所以我们这里面也应该把这些所有都分开


但是我们代码就先不列出来了。这个版本就是对于那种出现的最频繁的比如说呃 16 个 U8。或者是 1~10 个数字的 U16 或者 32 等等。这样都需要一个一个动态增长的堆上分配一个 vector 来表示。

在 Rust 里面有一个常用的 Crate 它可以用叫做 tinyvec,它可以用内联的就是 in ine 的。这个一个数字来表示。呃,小的这个 但是 tinyVecr 的效率是很差的,如果说你用 U16 呃,装一个比如说装呃 10 个字节的。就装 10 个有 16,感觉 10 个 u16 的应该是 20 个字节,对吧?每个 u16 就是两字节。

但是实际上呢你会发现它的 内存会比想象的大很多,就在这个 Dynamic 的结构。占了 48 字节,甚至 64 字节,甚至更多,就是因为 rust 的枚举他需要有一个叫做 tag 标签来来确定这个枚举是哪个类型。

同时呢他又是在 64 位机器还是按照 8 字节 对齐的。这样就使得这个结构会很快膨胀到 48 字节,甚至 64 字节,甚至更多。所以我们用一个简单的 trick。就是我们用统一所有的 Vec 都用 U8 来表示。这样我们大概可以呃在这个 40 字节的 dynamic 装下了大概 28 个这个 u 都可以 inline,

至于说是 U16,U32。或者 I16,I32 或者 F32 这些类型,我们可以通过我们在装入和装出的时候进行类型转换,没有任何开销的。实现这个紧凑的数据装载。


然后最终的这个数据结构大概就长这个样子。

#[derive(Debug, Default, Clone)]pub enum Dynamic {              //40 字节大小 的动态数据类型 绝大多数数据可以直接内联 这里的数据都有明确的类型#[default]    Null,    Bool(bool),    U8(u8),    I8(i8),    U16(u16),    I16(i16),    U32(u32),    I32(i32),                   //默认整数类型    U64(u64),    I64(i64),    F32(f32),                   //默认浮点类型    F64(f64),    String(SmolStr),    Bytes(TinyVec<[u8; 28]>),    VecU16(TinyVec<[u8; 28]>),    VecI16(TinyVec<[u8; 28]>),    VecU32(TinyVec<[u8; 28]>),    VecI32(TinyVec<[u8; 28]>),    VecF32(TinyVec<[u8; 28]>),    VecU64(Vec<u64>),    VecI64(Vec<i64>),    VecF64(Vec<f64>),    List(Vec<Dynamic>),    Object(BTreeMap<SmolStr, Dynamic>),}
复制代码

因为实现了这个,因为用用这个 tiinyvec 来表示那个。16 位的或者是 32 位的这种小的数据,所以我们需要在存取和装入装出的时候要进行转换。


首先要处理这种索引的访问。比如说 dynamic 的零,我们要能够正确的从这个所有的包括 0 号元素,index 的这个 10,我们要从所有从这个 vec 的那个 10 号里面取出数据。

我们最开始的时候是想用那个 fat 的那个 index index_mut to trait。但是那是基于 borrow 的借用,在我们的例子中,嗯,我们从取出来的时候就会很麻烦。所以我们考虑到这个 dynamic 数据结构是我们自己的数据结构,所以我们就是获取一个可以复制的是以及获取一个可变的 Vec 其中一个值。以后我们我们为了获取一个整数或者获取一个其他类型,我们可以再去增添更多的方法直接操作内容


impl Dynamic {    pub fn len(&self) -> usize {        match self {            Self::List(list)=> list.len(),            Self::Bytes(bytes)=> bytes.len(),            Self::VecI8(vec) | Self::VecI16(vec) | Self::VecU16(vec) | Self::VecI32(vec) | Self::VecU32(vec) | Self::VecF32(vec)=> vec.len(),            Self::VecI64(vec)=> vec.len(),            Self::VecU64(vec)=> vec.len(),            Self::VecF64(vec)=> vec.len(),            Self::Object(obj)=> obj.len(),            _=> { 1 }        }    }    pub fn get_dynamic(&self, idx: usize)-> Option<Self> {        match self {            Self::List(list)=> list.get(idx).cloned(),            _=> { None }        }    }}
复制代码


用户头像

Miracle

关注

三十年资深码农 2019-10-25 加入

还未添加个人简介

评论

发布
暂无评论
动态数据类型_Miracle_InfoQ写作社区