d的dip1000,1
DIP1000
:现代系统编程语言中的内存安全
现代高级语言
如D是内存安全
的,可防止用户意外读写未用内存
或破坏语言的类型系统
.D
的安全
子集提供保证,用@safe
来保证函数
内存安全.
@safe string getBeginning(immutable(char)* cString)
{
return cString[0..3];
}
编译器
拒绝编译此代码.无法知道cString
的三个字符切片会产生什么结果.可能是cString[0]==\0
的空串引用,或无\0
结尾的1/2
字符.此时,结果将是内存违规
.
@safe
不慢
注意,即使是低级系统编程
项目也应尽量用@safe
.不使用垃集
也可保证安全
.
如创建C
或C++
库接口,或为了性能运行时
避免垃集
.但,可在完全不用垃集
编写安全
代码.
D可这样,是因为内存安全子集
不会阻止原始内存访问
.
@safe void add(int* a, int* b, int* sum)
{
*sum = *a + *b;
}
尽管按完全相同未经检查
的C方式解引用
这些指针,但可编译
且是完全内存安全
的.
因为@safeD
禁止创建指向未分配内存区域
的int*
或float**
,如int*
可指向空(null)地址
,但这不是内存安全
问题,因为空地址
受操作系统
保护.
在损坏内存
前,解引用它们会使程序崩溃
.因为仅在请求
更多内存
或显式调用垃集
时才运行它,不涉及垃集
.
D切片
类似.运行时索引
时,动态检查索引
是否小于
长度,仅此而已.不会检查是否
指向合法
内存区域.内存安全
通过源头上
防止创建引用
非法内存切片
来实现的,如第一例
所示.再一次,与垃集
无关.
这启用了许多内存安全,高效且独立于垃集
的模式.
struct Struct
{
int[] slice;
int* pointer;
int[10] staticArray;
}
@safe @nogc Struct examples(Struct arg)
{
arg.slice[5] = *arg.pointer;
arg.staticArray[0..5] = arg.slice[5..10];
arg.pointer = &arg.slice[8];
return arg;
}
如上,在@safe
代码中,D
自由地允许你不检查
内存处理.arg.slice
和arg.pointer
可能在垃集
堆上,也可能在静态程序
内存中引用内存
.语言
不关心.
为指针和切片
分配内存,程序可能需要垃集
或管理
一些不安全内存
,但已分配
内存则不必.如果该函数需要垃集
,会因为@nogc
而编译失败.
然而…
这里有个历史设计
缺陷,即内存
也可能在栈上.如果稍微改变函数
会怎样?
@safe @nogc Struct examples(Struct arg)
{
arg.pointer = &arg.staticArray[8];
arg.slice = arg.staticArray[0..8];
return arg;
}
arg
构是值类型
.调用examples
时,复制内容(arg构)
到栈中,并且可在函数返回
后,覆盖它.staticArray
也是值类型.像结构中有十个
整数一样,与结构的其余部分
一起复制.返回arg
时,复制staticArray
内容到返回
值,但ptr
和slice
继续指向arg
,而不是返回的副本
!
但可解决.它允许像以前一样
,在函数中编写包括引用栈
的@safe
代码.甚至可安全(@安全)
编写一些以前用@system
的技巧.该修复
程序是DIP1000
.因此如果用最新的每晚dmd
编译此示例,会默认报告
已弃用警告
.
先生后死
DIP1000
增强了指针,切片和其他引用
的语言规则.dip1000.可用-preview=dip1000
预览编译器开关启用
新规则.
现有代码
需要一些更改
才能使用新规则
,因而默认不启用
该开关.未来将是默认
,因此最好现在
就启用它,并努力使代码
与它兼容.
基本思想
是限制引用(如数组或指针)
的生命期
.如果栈指针
存在时间不超过
指向的栈变量
,则它并不危险.普通
引用继续
存在,但只能引用
有无穷生命期
数据:即垃集
内存及静
或全局
变量.
开始
构造有限生命期引用
的最简单方法是用有限生命期
对象赋值它.
@safe int* test(int arg1, int arg2)
{
int* notScope = new int(5);
int* thisIsScope = &arg1;
int* alsoScope; // 非初始域
alsoScope = thisIsScope; // 但这就是了
// 先前定义变量,有更长生命期,所以禁止!
thisIsScope = alsoScope;
return notScope; // 好
return thisIsScope; // 错误
return alsoScope; // 错误
}
测试这些示例时,记住使用-preview=dip1000
编译器开关,并标记函数为@safe
.因为不检查非@safe
函数.
或,可显式用scope
关键字来限制引用
的生命期,这里.
@safe int[] test()
{
int[] normalRef;
scope int[] limitedRef;
if(true)
{
int[5] stackData2= [-1, -2, -3, -4, -5];
//stackData2在limitedRef前结束,因而禁止
limitedRef = stackData2[];
//你这样
scope int[]evenMoreLimited=stackData2[];
}
return normalRef; // 好.
return limitedRef; // 禁止.
}
如果不能返回有限
生命期引用
,则如何使用它们?简单.记住,仅保护数据地址
,而不是数据
自身.即有很多
方法可从函数中传递
出去域数据.
@safe int[] fun()
{
scope int[] dontReturnMe = [1,2,3];
int[] result=new int[](dontReturnMe.length);
//复制数据,而不是让结果`引用`受保护内存.
result[] = dontReturnMe[];
return result;
// 同上,简写.
return dontReturnMe.dup;
// 计算感兴趣数据
return
[
dontReturnMe[0] * dontReturnMe[1],
cast(int) dontReturnMe.length
];
}
取交互过程
目前,DIP1000
仅处理有限
生命期引用,但也可给函数参数
应用scope
存储类.保证了退出
函数后,不会使用该内存
,可按scope
参数使用局部数据引用
.
@safe double average(scope int[] data)
{
double result = 0;
foreach(el; data) result += el;
return result / data.length;
}
@safe double use()
{
int[10] data = [1,2,3,4,5,6,7,8,9,10];
return data[].average; // 工作!
}
开始,最好关闭自动推导
属性.一般,自动推导
很好,但它为所有参数安静的添加域
属性,很容易忘记
当前工作.从而更难学习.
始终明确
指定返回类型
(或void/noreturn
)来避免:用@safe const(char[]) fun(int* val)
而不是@safe auto fun(int* val)或@safe const fun(int* val)
.该函数也不必是模板或在模板内
.未来研究scope
自动推导.
scope
允许处理栈指针和数组
,但禁止
返回它们.目的呢?输入return scope
属性:
//作为字符数组,串也适合`DIP1000`.
@safe string latterHalf(return scope string arg)
{
return arg[$/2 .. $];
}
@safe string test()
{
//在静态程序内存中分配
auto hello1 = "Hello world!";
//在栈上分配,从`hello1`复制
immutable(char)[12] hello2 = hello1;
auto result1 = hello1.latterHalf; // 好
return result1; // 好
auto result2 = hello2[].latterHalf; // 好
//很好!result2是域,不能返回它
return result2;
}
中域
参数检查
传递给它们的参数
是否为scope
.是,则按不超过任一中域
参数的scope
值对待返回值
.否(都不是域
),则按可自由复制
的全局引用
对待返回值.像scope
,return scope(中域)
是保守的.即使不返回returnscope
保护地址
,编译器
仍会像返回
一样,检查调用点生命期
.
scope
是浅的
@safe void test()
{
scope a = "first";
scope b = "second";
string[] arr = [a, b];
}
在test
中,不会编译初化arr
.因为如果需要
,语言会在初化
时自动加scope
到变量中,这令人惊讶.
但是,请考虑scope string[] arr
上的scope
保护什么.可保护两样:数组中串地址,或串中字符地址
.为了该赋值安全,scope
必须保护串中字符
,但它只保护顶级引用
,即数组
中串.因此,该示例不工作.现在更改arr
为静态数组:
@safe void test()
{
scope a = "first";
scope b = "second";
string[2] arr = [a, b];
}
因为静态数组
不是引用
,这工作
.在栈上就地
分配所有元素内存
(即,栈包含元素
),与包含引用
存储在其他地方元素
的动态数组
不一样.
静态数组为scope
时,按scope
对待元素.且如果arr
非域
,则该示例无法编译
,因此可推导scope
.
实用技巧
需要时间来理解DIP1000
规则,许多人不愿学习.第一个也是最重要
的技巧是:尽量避免非@safe
代码.
当然,该建议并不新鲜,但对DIP1000
来说更重要
.总之,在非@safe
函数中,语言不会检查
非安全函数的域
和中域
有效性,而调用
这些函数时,编译器
会假定满足了这些属性
.
因而在不安全
代码中,域
和中域
危险.但是只要避免标记为@trusted
,D
代码几乎不会造成损害
.@safe
代码中滥用DIP1000
可能会导致不必要
编译错误,但它不会损坏内存
和其他错误.
值得一提的第二个
重点是,如果函数属性
仅接收静态
或GC
分配数据
,则不必用域
和中域
.
许多语言禁止
程序员引用栈.D
可以这样,不代表必须
用栈.这样,像DIP1000
出来之前,不必花费更多
时间来解决编译器错误
.如果想用栈,就注解
函数.这样,不破坏接口
.
下一步?
本文,说明了在DIP1000
中使用数组和指针
.原则上,还允许读者用类和接口
使用DIP1000
.
唯一要了解的是包括
成员函数中this
指针的类引用
,同DIP1000
一起就像指针
一样使用.
DIP1000
对ref
函数参数,结构和联
还有些特性.还会深入探讨DIP1000
如何处理非@safe
函数和属性自动推导
.目前,计划是再写两篇
文章.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现