1个不稳定版本
0.3.0 | 2022年7月6日 |
---|---|
0.1.0 |
|
#129 in #proof
87KB
963 行
欢迎来到🦀Rustaceans的区块链之家
该仓库是Substrate框架的先决条件,并且也是练习的好机会。
该仓库包含Rust语法、配置,目标是创建类似的一个简化代码,因为这是为了提供一个区块链的测试环境。
另一个原因是想要使用一些Rust语言的功能,在学完Rust后我想实现它。
在项目的主要运行程序中使用了json,以便将json事务作为离线区块链消费。
随着你继续学习材料,你可以看到(未来工作),这意味着你可以将这些概念添加到项目中。我有一些关于你可以着手完成未来工作的想法。
智能合约、多重签名、RPC、改进CLI、以及你可以实现的一些东西(不要担心,因为你的大部分工作将合并到主分支。我们不会创建框架或完整的区块链,因为我们只是需要更多学习和使用案例)
当前工作与以前工作的不同之处
我尽量正确使用基本概念,例如,我们都知道任何块都没有副本,所以因为这个原因我们称之为区块链!与GitHub上许多仓库(测试环境/简化项目/非生产性)几乎使用Rust语言的Copy/Clone属性创建块不同。以下是一些导致项目不同的功能。
当前状态:正在重构中,有贡献者参与
如何贡献 简单
文档包
使用说明
DIFFICULTY={difficulty} cargo {mode} {file name}
{difficulty}: (可选键环境)值默认为0x00ffffffffffffffffffffffffffffff。它必须是32字节。
{mode}: 宏,字符串,文件/默认模式是宏json模式。
{macro, string} 在项目中,你无法访问或操作,除非通过获取项目。serde_json支持字符串和基于调用库的宏。{file} json文件是外部.json文件,你可以将其设置为命令行
{file name} 项目索引目录sample-bolocks.json
示例
cargo build
cargo run
RUST_LOG=INFO DIFFICULTY=0x00000fffffffffffffffffffffffffff time cargo run file sample-bolocks.json
DIFFICULTY=0x00000fffffffffffffffffffffffffff time cargo run macrojson
DIFFICULTY=0x00000fffffffffffffffffffffffffff time cargo run stringjson
time cargo run
cargo watch -x run
cargo test
使用时间和监视是可选的,取决于你的目的
安装bin的说明
curl -LSfs https://github.com/armanriazi/armanriazi/blob/main/install-0.sh | sh -s -- --git armanriazi/rust-scratch-blockchain
功能
-
模块化
-
自定义错误处理
-
Json & String数据反序列化
-
函数式编程(闭包)
-
密码学散列算法SHA-256
-
单元与集成测试(结构-未来需要更多时间)
-
配置文件(DevOps-未来需要更多时间)
使用到的概念
- [✓] 缓存技术•惰性•评估
我们可以创建一个结构体,用来保存闭包及其调用结果。
结构体只有在需要结果值时才会执行闭包,并且它会缓存结果值,这样我们的其他代码就不需要负责保存和重用结果。
FnOnce消耗其从封装作用域捕获的变量,称为闭包的环境。为了消耗捕获的变量,闭包必须对这些变量进行所有权转移,并在定义时将它们移动到闭包中。名称中的Once部分表示闭包不能多次拥有相同的变量,因此只能调用一次。
FnMut可以通过可变借用值来改变环境。
Fn从环境不可变地借用值。FnOnce:获取整个值。FnMut:获取可变引用。Fn:获取常规引用。
- [✓] 转换
解引用转换是Rust在函数和方法参数上执行的一种便利操作。解引用转换仅适用于实现了Deref特质的类型。解引用转换将此类类型转换为另一个类型的引用。例如,解引用转换可以将 &String 转换为 &str,因为String实现了Deref特质,使其返回 &str。
Deref::deref需要插入的次数在编译时确定,因此利用解引用转换没有运行时开销!类似于如何使用Deref特质来重写不可变引用的 * 操作符,你可以使用DerefMut特质来重写可变引用的 * 操作符。Drop特质在实现智能指针时几乎总是被使用。例如,当Box被丢弃时,它将释放堆上Box指向的空间。
注意,我们不需要显式调用drop方法。
- [✓] DST•Or•无尺寸类型
DSTs或无尺寸类型:str(但不是 &str-因此,虽然 &T 是一个存储 T 所在内存地址的单个值,但 &str 是两个值:str的地址及其长度。Rust有一个名为Sized的特质,用于确定类型的大小是否在编译时已知。这个特质对于所有在编译时大小已知的类型都会自动实现。此外,Rust隐式地为每个泛型函数添加了Sized的界限。
- [✓] 操作
-> Methods are functions that are coupled to some object.
从语法角度来看,这些只是不需要指定其一个参数的函数。而不是调用open()并传递一个File对象作为参数(read(f, buffer)),方法允许主对象在函数调用中使用点操作符隐式地在其中(f.read(buffer))。
方法和函数之间存在许多理论上的差异,但这些计算机科学主题的详细讨论可以在其他书籍中找到。简而言之,函数被视为纯函数,其行为仅由其参数决定。方法是固有的不纯,因为其中一个参数实际上是副作用。这些是模糊的水域。函数完全能够自己执行副作用。此外,方法是使用函数实现的。并且,为了给一个异常加上一个异常,对象有时实现静态方法,这些方法不包含隐式参数。Rust程序员使用impl块来定义方法。
- [✓] 借用检查器
借用检查器检查所有对数据的访问是否合法,这允许Rust防止安全问题的发生。了解它是如何工作的,至少可以加快你的开发时间,帮助你避免与编译器的冲突。更重要的是,学会与借用检查器一起工作,可以让你有信心构建更大的软件系统。它支撑了“无畏并发”这个术语。
- [✓] 借用检查器•生命周期
-> Lifetime=Timetolive=Subset of their scope
在Rust中,在编译引用具有生命周期的引用之前,做出假设以确定您的实验是否可以通过借用检查器。这是引用有效的范围。大多数情况下,生命周期的注解是隐式的,就像大多数情况下类型也是隐式推断的。当存在多种可能的类型时,我们必须对类型进行注解。类似地,当引用的生命周期可以通过几种不同的方式相关联时,我们必须对生命周期进行注解。
生命周期的主要目的是防止悬垂引用,这会导致程序引用它本不应该引用的数据。在Rust中,所有引用都具有生命周期,即使它们没有明确注解。编译器能够隐式地分配生命周期。
一个值的生命周期是访问该值有效行为的期间。函数的局部变量在函数返回之前存活,而全局变量可能在整个程序的生命周期内存活。
所有权的概念相当有限。所有者在其值的生命周期结束时进行清理。
尽管每个参数都有一个生命周期,但这些检查通常是不可见的,因为编译器可以自己推断大多数生命周期。
绑定到给定生命周期的所有值必须存活到该生命周期绑定值的最后访问。
调用函数时不需要生命周期注解。
生命周期注解不会改变任何引用的持续时间。就像函数可以接受任何类型,当签名指定了泛型类型参数时,函数可以通过指定泛型生命周期参数来接受任何生命周期的引用。
生命周期注解描述了多个引用的生命周期之间的关系,而不影响生命周期。
生命周期注解表明引用first和second都必须与那个泛型生命周期一样长。
函数或方法参数上的生命周期称为输入生命周期,返回值上的生命周期称为输出生命周期。
尽管每个参数都有一个生命周期,但这些检查通常是不可见的,因为编译器可以自己推断大多数生命周期。
绑定到给定生命周期的所有值必须存活到该生命周期绑定值的最后访问。
调用函数时不需要生命周期注解。
使用两个生命周期参数(a和b)表示i和j的生命周期是解耦的。
fn add_with_lifetimes<'a, 'b>(i: &'a i32, j: &'b i32) -> i32 {}
该使用的生命周期
从某个位置以某种方式首次使用到该使用停止的LOC(存在时间或代码行)。
该值的生命周期
从创建值到该值被丢弃的LOC(或实际时间)。
可能当讨论打开文件描述符时有用,但在此处不相关。
最终,生命周期语法是关于连接函数的各种参数和返回值的生命周期。一旦它们连接起来,Rust就有足够的信息来允许内存安全的操作,并禁止创建悬垂指针或其他违反内存安全性的操作。
- [✓] 悬垂
生命周期的主要目的是防止悬垂引用,它具有外部范围和内部范围。在函数的返回部分,原始类型需要定义为(&'a或&'static)
- [✓] 泛型
您可能想知道在使用泛型类型参数时是否有运行时成本。好消息是使用泛型类型不会使您的运行速度比使用具体类型慢。
Rust通过在编译时对使用泛型的代码执行单态化来实现这一点。单态化是通过填充在编译时使用的具体类型来将泛型代码转换为特定代码的过程。每种编程语言都有用于有效地处理概念复制的工具。
在 Rust 语言中,泛型就是一种这样的工具。泛型是对具体类型或其他属性的抽象替代。当我们编写代码时,可以表达泛型的行为或它们如何与其他泛型相关联,而无需知道编译和运行代码时将是什么类型。
静态分发(已通过)
-> Monomorphization
分发是确定在涉及多态性时实际运行哪种特定版本代码的机制。分发的两种主要形式是静态分发和动态分发。虽然 Rust 倾向于静态分发,但它也通过称为“特例对象”的机制支持动态分发。当 Rust 编译此代码时,它执行单态化。
单态化版本的代码如下所示。泛型 Option 被编译器创建的具体定义所替换:在编译期间,多态函数(或任何多态实体)的版本被称为单态化。
因为 Rust 将泛型代码编译成指定每个实例类型的代码,所以我们使用泛型时不需要运行时成本。当代码运行时,它表现得就像我们手动复制每个定义一样。单态化过程使 Rust 的泛型在运行时非常高效。这与动态分发相反
- [✓] 动态分发
单态化产生的代码执行静态分发,这意味着编译器在编译时知道你正在调用哪个方法。这与动态分发相反,动态分发是指编译器在编译时无法确定你正在调用哪个方法。在动态分发的情况下,编译器生成的代码在运行时会确定要调用哪个方法。
当我们使用特例对象时,Rust 必须使用动态分发。编译器不知道可能与使用特例对象的代码一起使用的所有类型,因此它不知道要调用哪个类型上实现的方法。相反,在运行时,Rust 使用特例对象内部的指针来确定要调用哪个方法。当发生这种查找时,会有运行时成本,而在静态分发中则不会发生这种成本。动态分发还阻止编译器选择内联方法的代码,这进而防止了一些优化。
- [-] 全覆盖实现
任何类型出现未覆盖的实现。impl Foo for T, impl Bar for T, impl Bar
for T, 以及 impl Bar for Vec 被视为全覆盖实现。
我们还可以为任何实现另一个特质的类型有条件地实现一个特质。满足特质界限的任何类型的特质实现被称为全覆盖实现,并在 Rust 标准库中被广泛使用。例如,标准库为任何实现 Display 特质的类型实现了 ToString 特质。
- [✓] 界限(语法)
界限是对类型或特质的约束。例如,如果将界限放置在函数的参数上,传递给该函数的类型必须遵守该约束。
- [✓] 特质
我们可以使用特质以抽象的方式定义共享行为。我们可以使用特质界限来指定泛型类型可以是任何具有某些行为的类型。特质与其他语言中常称为接口的功能相似,尽管有一些区别。
什么是特质?特质是一种类似于接口、协议或合同的语言功能。如果你有面向对象编程的背景,可以将特质视为一个抽象基类。如果你有函数式编程的背景,Rust 的特质与 Haskell 的类型类非常相似,这些类型类也支持大多数面向对象语言中常见的继承形式。目前,要记住的是,特质代表共同行为(或如 println! 这样的可重用代码),类型可以通过 impl Trait for Type 语法选择加入这些行为。
在方法签名之后,我们使用分号而不是在大括号内提供实现。
此接口由关联项组成,分为三种类型:函数、类型、常量。
所有特性都定义了一个隐式类型参数Self,它指的是“实现此接口的类型”。
特性函数可以通过用分号替换函数体来省略函数体。这表示实现必须定义该函数。如果特性函数定义了主体,则此定义充当未重写的任何实现的默认值。同样,关联常量可以省略等号和表达式,表示实现必须定义常量值。关联类型永远不能定义类型,类型只能在实现中指定。
- [✓] 多态
在结构体或枚举中,结构体字段中的数据和impl块中的行为是分开的,而在其他语言中,数据和行为的组合通常被标记为对象。然而,特性对象在其他语言中的对象更像是它们结合了数据和行为。
- [✓] 展开迭代
这是一种优化,它移除了循环控制代码的开销,并为循环的每次迭代生成重复的代码。
- [✓] 绑定•匹配
编译器自动引用Some,由于我们在借用,所以name也自动绑定为ref name。如果我们是修改的
//https://blog.rust-lang.net.cn/2018/05/10/Rust-1.26.html#nicer-match-bindings
// `self` has type `&List`, and `*self` has type `List`, matching on a
// concrete type `T` is preferred over a match on a reference `&T`
// after Rust 2018 you can use self here and tail (with no ref) below as well,
// rust will infer &s and ref tail.
- [✗] 数据竞争•Rustaceans
注意:与使用 & 相反的是解引用,它是通过解引用运算符 * 实现的。
[-] 纳尼(哲学)
浮点类型包括“不是一个数字”值(在Rust语法中表示为NAN值),以处理这些情况。
NAN值会毒害其他数值。几乎所有与NAN交互的操作都会返回NAN。另一件需要注意的事情是,根据定义,NAN值永远不相等。编程语言设计通常被认为包括哪些功能,但你排除的功能同样重要。Rust没有许多其他语言所具有的空值特征。空值是一个表示那里没有值的值。在具有空值的语言中,变量总是处于两种状态之一:空值或非空值。在他的2009年演讲“空引用:十亿美元的错误”中,空值的发明者托尼·霍尔说:“我称之为我的十亿美元的错误。当时,我正在为面向对象语言设计第一个全面参考类型系统。我的目标是确保所有引用的使用都是绝对安全的,编译器会自动进行检查。但我无法抵制添加空引用的诱惑,仅仅因为它的实现非常简单。这导致了无数的错误、漏洞和系统崩溃,这可能在过去四十年中造成了十亿美元的痛苦和损失。”
为了进行防御性编程,请使用is_nan()和is_finite()方法。诱发的崩溃,而不是静默地继续数学错误,使您能够调试接近问题的原因。以下展示了使用is_finite()的方法
- [✓] 复制(字面量)
复制指针、长度和容量而不复制数据的概念可能听起来像是浅复制。
如果我们确实想深度复制String的堆数据,而不仅仅是栈数据,我们可以使用一个常见的方法,即clone
- [✓] 语义(字面量)
原始类型被认为是具有复制语义,而所有其他类型都具有移动语义。通过将这些类型包装在其他类型中为类型添加更多功能(例如,引用计数语义而不是移动语义),通常降低它们的运行时性能。
- [✓] 零成本抽象(字面量)
这种表现方式之一是在结构体值周围不添加额外的数据。
- [✓] 一致性(字面量)
-> Orphan = Trait•External•Implement
但是,我们不能在外部类型上实现外部特性。例如,我们无法在我们的聚合器crate中为Vec实现Display特性,因为Display和Vec都定义在标准库中,并不属于我们的聚合器crate。这种限制是程序属性之一,称为一致性,更具体地说是孤儿规则,之所以这样命名是因为父类型不存在。这条规则确保了别人的代码不会破坏你的代码,反之亦然。
如果没有这条规则,两个crate可能会为同一类型实现相同的特性,而Rust将不知道使用哪个实现。
在记录时,保留了任务/函数/方法中跟踪数据的上下文一致性。
例如,当然,新实例的struct,你可能已经知道,struct然后你可以在方法中总结你的struct。
- [✓] 行话(字面意思)
函数式编程术语:“将x cons到y”非正式地意味着通过将元素x放在新容器的开头,然后是容器y来构造一个新的容器实例。其他更复杂的数据类型在各种情况下都很有用,但通过从cons列表开始,我们可以探索盒子如何让我们定义递归数据类型而不受太多干扰。
- [✓] 重构(字面意思)
重构的一个替代方案是简单地复制值。然而,通常不鼓励这样做,但在紧急情况下可能会有所帮助。例如,原始类型如整数就是这样一个例子。原始类型对于CPU来说复制成本低——事实上,成本低到Rust总是复制这些类型,如果它会担心所有权被移动。
类型可以选择两种复制模式:克隆和复制。
- [✓] 模式•Newtype
使用Newtype模式在外部类型上实现外部特性 '类型的外部包装':Vec的一部分被注意到。我们可以创建一个包装器struct来持有Vec的实例;然后我们可以实现包装器上的Display,并使用Vec的值。使用这种技术的缺点是包装器是一个新类型,所以它不具有它所持有的值的那些方法。我们必须直接在包装器上实现Vec的所有方法,使方法委派给self.0,这样我们就可以像Vec一样处理包装器。
如果我们希望新类型拥有内部类型的所有方法,则必须实现Deref特性(如果我们不希望包装器类型具有内部类型的所有方法——例如,为了限制包装器类型的行为——我们就必须手动实现我们想要的方法。)
- [✗] 模式•设计•内部(未来工作)
Rust中的一种设计模式,允许你在存在不可变引用的情况下修改数据;通常,这种行为会被借用规则禁止。为了修改数据,该模式使用数据结构内部的不安全代码来弯曲Rust通常用于修改和借用的规则。
遵循内部可变性模式的RefCell类型。
与Rc不同,RefCell表示对其所持数据的单一所有权。那么,是什么使得RefCell与Box这样的类型不同?回想一下借用规则...
与Rc类似,RefCell仅适用于单线程场景,如果你尝试在多线程环境中使用它,将会在编译时得到错误。在任何给定时间,你只能有一个(但不能同时有两个)可变引用或任意数量的不可变引用。引用必须始终有效。
- [✗] 类型•包装器(未来工作)
-> Wrapper type = Reference-Counted Value = Shared Ownership = Track valid references
使用包装器类型,这比默认提供的更灵活。然而,这需要在运行时付出代价以确保Rust的安全保证得到维护。另一种说法是,Rust允许程序员选择垃圾回收。
- [✗] 内存•泄漏(未来工作)
-> Managing Memory Leak
Rust的内存安全保证使得意外创建无法清理的内存(称为内存泄漏)变得困难,但并非不可能。与在编译时禁止数据竞争不同,防止内存泄漏并不是Rust的保证之一,这意味着在Rust中内存泄漏是内存安全的。
我们可以看到,Rust通过使用Rc和RefCell允许内存泄漏:可以创建循环引用,其中项目互相引用。这会创建内存泄漏,因为循环中每个项目的引用计数永远不会达到0,值也永远不会被丢弃。
- [✗] Mem•Doublefree(未来工作)
这是一个问题:当s2和s1(s2是s1的副本意味着两个不同的指针和相同的数据)超出作用域时,它们都会尝试释放相同的内存。这被称为双重释放错误,是我们之前提到的内存安全错误之一。重复释放内存可能导致内存损坏,这可能导致安全漏洞。
- [✗] Mem•Deallocating•or•RAII(未来工作)
注意:在C++中,这种在项目生命周期结束时释放资源的模式有时被称为资源获取即初始化(RAII)。如果你使用过RAII模式,Rust中的drop函数可能会让你感到熟悉。
- [✗] Thread(未来工作)
你代码的不同线程中哪些部分会运行。这可能导致问题,例如
竞争条件,其中线程以不一致的顺序访问数据或资源 哑铃,其中两个线程正在等待对方完成使用另一个线程拥有的资源,从而阻止两个线程继续运行 只在特定情况下发生且难以重现和可靠修复的错误。
- [✗] Thread•Strateges(未来工作)
-> Priority Performance
Stealing_Join:当有空闲CPU处理时并行执行代码。
当从线程池外部调用join时,调用线程将在闭包在池中执行时阻塞。当在池内调用join时,调用线程仍然积极参与线程池。它将首先执行闭包A(在当前线程上)。在执行过程中,它将宣布闭包B可供其他线程执行。一旦闭包A完成,当前线程将尝试执行闭包B;然而,如果闭包B已被偷走,那么它将等待窃贼完全执行闭包B时寻找其他工作。(这是典型的工作窃取策略)。Send是必需的,因为我们经常从快速函数(thread a)跳转到部分函数(thread b)。
Atomic:类型提供线程之间的原始共享内存通信,是其他并发类型的构建块。此模块定义了包括AtomicBool、AtomicIsize、AtomicUsize、AtomicI8、AtomicU16等在内的原始类型的原子版本。当正确使用时,原子类型提供操作以同步线程之间的更新。每个方法都接受一个Ordering,它表示该操作的内存屏障的强度。这些顺序与C++20原子顺序相同。有关更多信息,请参阅nomicon。
原子变量可以在线程之间安全共享(它们实现Sync),但它们本身不提供共享机制,并遵循Rust的线程模型。共享原子变量的最常见方法是将它放入Arc(原子引用计数共享指针)。原子类型可以存储在静态变量中,使用类似于AtomicBool::new的常量初始化器进行初始化。原子静态变量通常用于懒惰的全局初始化。
Spin_Loop_Yeild 也称为忙循环和自旋循环-如果你想暂停线程以短时间睡眠,或者如果你的应用程序对时间敏感,请使用自旋循环
- [✗] Unsafe•Extern•Mangling(未来工作)
> Mangling is when a compiler changes the name we’ve given a function to a different name that contains more information for other parts of the compilation process to consume but is less human readable. Every programming language compiler mangles names slightly differently, so for a Rust function to be nameable by other languages, we must disable the Rust compiler’s name mangling.
- [✓] Interior•Mutability•Pattern
RefCell:允许在任何时间点拥有多个不可变借用或一个可变借用。在不可变值内部修改值是内部可变性模式。内部可变性是Rust中的一个设计模式,允许即使在有对该数据的不可变引用的情况下也能修改数据;通常,这种操作会因借用规则而被禁止。要修改数据,此模式在数据结构内部使用不安全代码来弯曲Rust通常的修改和借用规则。遵循内部可变性模式的RefCell类型。与Rc不同,RefCell类型表示它所持有的数据具有单一所有权。那么,RefCell与Box这样的类型有什么区别呢?回想一下借用规则,与Rc类似,RefCell仅用于单线程场景,如果在多线程环境中尝试使用它,将给出编译时错误。在任何给定时间,你可以拥有一个可变引用(但不能同时拥有)或任意数量的不可变引用。引用必须始终有效。
- [✗] 面向对象编程•状态•设计模式(未来工作)
-> We can used it for smart contracts so we will need to implemented smart contracts
使用状态模式意味着当程序的业务需求发生变化时,我们不需要更改包含状态的值的代码或使用该值的代码。我们只需更新其中一个状态对象内部的代码,以更改其规则或可能添加更多的状态对象。
例如,Post类型。此类型将使用状态模式,并持有表示帖子的各种状态之一(草稿、等待审核或发布)的值。从一个状态切换到另一个状态将由Post类型内部管理。状态的变化是响应我们的库用户在Post实例上调用的方法,但他们不必直接管理状态变化。此外,用户无法犯错误,例如在帖子审核前发布帖子。
- [✗] 超能力(未来工作)
如果Rust编译器没有足够的信息来确保,它将拒绝该代码。在这些情况下,您可以使用不安全代码来告诉编译器,“相信我,我知道我在做什么。”缺点是您自己承担风险:如果您不正确地使用不安全代码,可能会发生因内存不安全引起的问题,例如空指针解引用。
您可以在不安全Rust中执行五种操作,称为不安全超能力,而在安全Rust中不能执行。这些超能力包括以下能力
取消引用原始指针
调用不安全函数或方法
访问或修改可变静态变量
实现不安全特质
访问联合的字段
调用unsafe()会导致程序崩溃。
将unsafe视为一个警告标志,而不是表示您正在开始任何事情非法的指标。不安全意味着“在所有时间提供与C相同级别的安全性。”
如果您仍然可以通过(通过unsafe)访问它们,它们可能仍然看起来像有效的S,但任何尝试将它们用作有效S的操作都是未定义的行为。↓ https://cheats.rs/#unsafe-unsound-undefined-dark 力量的另一面 尽量避免 "unsafe {}", 通常没有它的更安全、更快的解决方案。例外:FFI。人们会犯错误,错误会发生,但通过要求这些五个不安全操作必须在不安全块中注释,您将知道与内存安全相关的任何错误都必须在不安全块内。将不安全代码隔离到尽可能小,最好在安全抽象中包含不安全代码,并提供安全API,我们将在本章后面讨论不安全函数和方法时讨论。
标准库的部分部分作为经过审计的不安全代码的安全抽象实现。将不安全代码包装在安全抽象中可以防止不安全的使用泄露到您或您的用户可能想要使用使用不安全代码实现的功能的所有地方,因为使用安全抽象是安全的。
贡献者
nom 是多年来许多贡献者工作的成果,非常感谢您的帮助!
贡献者感谢
已解决的问题
依赖项
~7–18MB
~251K SLoC