dip1000,2
原文
上一篇文章显示了如何用新的DIP1000规则让切片和指针内存安全的引用栈.但是D也可其他方式引用栈.
面向对象实例
前面说过,如果了解DIP1000如何同指针工作,那么就会了解它如何同类工作.示例:
@safe Object ifNull(return 域 Object a, return 域 Object b)
{
return a? a: b;
}
中域与如下相同:
@safe int* ifNull(return 域 int* a, return 域 int* b)
{
return a? a: b;
}
原则是:如果参数列表中对象应用域/中域存储类,则按参数是实例指针一样保护对象实例地址.从机器码上看,它是实例指针.
普通函数,仅此而已.类或接口成员函数呢?如下:
interface Talkative
{
@safe const(char)[] saySomething() 域;
}
class Duck : Talkative
{
char[8] favoriteWord;
@safe const(char)[] saySomething() 域
{
import std.random : dice;
// 如下不行.
// return favoriteWord[];//禁止.1
// 如下可以
return favoriteWord[].dup;
// 可返回完全不同的,
// 头2/5返回第1项
// 接着2/5返回第2项
// 剩下1/5返回第3项
return
[
"quack!",
"Quack!!",
"QUAAACK!!!"
][dice(2,2,1)];
}
}
成员函数名前后的域,按域标记this,来防止本从函数中泄漏.因为保护了实例地址,所以禁止直接引用字段地址逃逸.(.1)是存储在类实例中的静态数组,返回切片会直接引用它.而favoriteWord[].dup返回不在类实例中的数据副本,因而可以.
或,可用中域(允许直接返回)替换Talkative.saySomething和Duck.saySomething中的域.
DIP1000和里氏替换原则
里氏替换原则,总之,继承函数比父函数更严格.DIP1000就是这样.规则如下:
1,如果父函数中参数(包括隐式this引用),没有DIP1000属性,则子函数可指定自己为域或中域.
2,如果在父中指定了域参数,则必须在子中同样指定域.
3,如果在父中指定了中域参数,则同样必须在子中指定域/中域.
如果无属性,调用者得不到保证;该函数可能会存储参数.如果有中域,调用者可假设除了返回值外没有存储参数地址.用域,保证不存储地址,这是更强大的保证.示例:
class C1
{ double*[] incomeLog;
@safe double* imposeTax(double* pIncome)
{
incomeLog ~= pIncome;
return new double(*pIncome * .15);
}
}
class C2 : C1
{
// 语言角度正确
override @safe double* imposeTax
(return scope double* pIncome)
{
return pIncome;
}
}
class C3 : C2
{
// 正确
override @safe double* imposeTax
(scope double* pIncome)
{
return new double(*pIncome * .18);
}
}
class C4: C3
{
//不行,C3.imposeTax是`域`,这放松了.
override @safe double* imposeTax
(double* pIncome)
{
incomeLog ~= pIncome;
return new double(*pIncome * .16);
}
}
ref特殊指针
讲了指针和数组,然后是在DIP1000中使用structs和union.引用struct或union时,工作方式与引用其他类型时相同.但是在D中,指针和数组并不是使用结构的正规方式.它们一般按值传递,或在绑定到ref参数时按引用传递.现在解释DIP1000如何同ref工作.
它们不像指针那样.一旦理解ref了,就可用DIP1000完成其他无法做到工作.
简单ref int参数
最简单使用ref方法可能如下:
@safe void fun(ref int arg) {
arg = 5;
}
何意?ref就像int*pArg,在内部是指针.但在源码中像值一样使用.arg=5等价于*pArg=5.此外,客户好像参数是按值传递的一样调用函数:
auto anArray = [1,2];
fun(anArray[1]); // 或UFCS: anArray[1].fun;
// 现在是[1, 5]
而不是用fun(&anArray[1]).与C++引用不同,D引用可为无效(null),但如果null不是用&读取地址,则null按段错误立即终止.
int* ptr = null;
fun(*ptr);
...
编译,但运行时崩溃,因为给fun内部赋值空地址.
总是防止ref变量地址逃逸.
@safe void fun(ref int arg){arg = 5;}
//==
@safe void fun(scope int* pArg){*pArg = 5;}
因而,
@safe int* fun(ref int arg){return &arg;}
不会编译,因为
@safe int* fun(scope int* pArg){return pArg;}
也不会.
然而,有中引用存储类,与中域一样,禁止其他形式逃逸,但允许返回参数地址,
@safe int* fun(return ref int arg){return &arg;}
上面,是有效的.
引用引用
引用,整或类似类型,比指针更干净,但ref引用引用时,更强大.如,引用指针或类.可应用域或中域至引用的引用,如:
@safe float[] mergeSort(ref return scope float[] arr)
{//中域保证.
import std.algorithm: merge;
import std.array : Appender;
if(arr.length < 2) return arr;
auto firstHalf = arr[0 .. $/2];
auto secondHalf = arr[$/2 .. $];
Appender!(float[]) output;
output.reserve(arr.length);
foreach
(
el;
firstHalf.mergeSort
.merge!floatLess(secondHalf.mergeSort)
) output ~= el;
arr = output[];
return arr;
}
@safe bool floatLess(float a, float b)
{
import std.math: isNaN;
return a.isNaN? false:
b.isNaN? true:a<b;
}
mergeSort这里保证不会泄露除了返回值之外的floats的地址.arr与中域float[]arr保证相同.但同时,因为arr是ref参数,mergeSort可改变传递给它的数组.然后客户可写:
float[] values = [5, 1.5, 0, 19, 1.5, 1];
values.mergeSort;
用非ref参数,客户就要values=values.sort(虽然这里不用引用是合理的).而指针无法这样,因为中域float[]*arr会保护数组元数据地址(数组的length和ptr字段),而不是内容地址.
也可给域引用提供可返回的ref参数,由于该示例有单元测试,在编译二进制文件中,记住使用-unittest编译标志.
@safe ref Exception nullify(return ref scope Exception obj)
{
obj = null;
return obj;
}
@safe unittest
{
scope obj = new Exception("Error!");
assert(obj.msg == "Error!");
obj.nullify;
assert(obj is null);
// nullify按引用返回.可赋值给返回值
obj.nullify = new Exception("Fail!");
assert(obj.msg == "Fail!");
}
这里返回传递给nullify参数地址,但仍保证其他通道不会泄露对象指针和类实例的地址.
return不强制返回遵守ref或域.
void* fun(ref scope return int*)
上面何意?规范指出,按引用 中对待非中域.因此,等价于:
void* fun(return ref scope int*)
但是,这仅适合有个ref时.
void* fun(scope return int*)
//==
void* fun(return scope int*)
甚至:
void* fun(return int*)
//==
void* fun(中域 int*)
成员函数和ref
一般需要仔细考虑,ref和return ref来跟踪保护哪个地址及可返回.熟悉后,理解structs和unions如何同DIP1000工作就非常简单了.
与类主要区别在,this引用只是类成员函数中的普通类引用,而this而在构或联成员函数中是ref StructOrUnionName.
union Uni
{
int asInt;
char[4] asCharArr;
//返回值包含联的引用,无论如何,不会逃逸引用
@safe char[] latterHalf() return
{
return asCharArr[2 .. $];
}
// 该参隐式引用,
// 中值不引用该联,我们不会泄露它.
@safe char[] latterHalfCopy()
{
return latterHalf.dup;
}
}
注意,return ref不应与this参数一起使用.无法解析:
char[] latterHalf() return ref
语言必须理解
ref char[] latterHalf() return
意思:返回值是ref引用.而"ref"是多余的.
注意,在此没有使用域键关字.就像域 引用 整或域 整参数是无意义的,因为它不包含引用,因而域在该联合中毫无意义.域仅对在别处引用内存类型才有意义.
域在构/联中等价于在静态数组中.即成员引用的内存不会逃逸.示例:
struct CString
{
// 需要用挂名成员把`指针`放在`匿名联`中,否则`@safe`用户代码可赋值`ptr`为不在C串中的字符.
union
{
// D编译器`优化`空串字面为`空指针`,必须这样做才能使`.init`值真正指向`'\0'`.
immutable(char)* ptr = &nullChar;
size_t dummy;
}
//在构造器中,"`返回值`"是构造的`数据对象`.因此,此处返回`域`确保此结构不会比内存中的`arr`长.
@trusted this(return scope string arr)
{
//注意:不要`正常断定`!可能会从`发布`版本中删除,但是该`断定`对`内存安全`来说是必要的,所以要用`assert(0)`来代替,永远不会删除它.
if(arr[$-1] != '\0') assert(0, "非C string!");
ptr = arr.ptr;
}
//`返回值`引用`此结构`中成员相同内存,但不会通过`其他方式`泄漏`它的引用`,因此返回`域`.
@trusted ref immutable(char) front() return scope
{
return *ptr;
}
//未传递数组指针引用.
@trusted void popFront() scope
{
//否则用户可能会跳出串末尾然后读它!
if(empty) assert(0, "越界!");
ptr++;
}
// 同样.
@safe bool empty() scope
{
return front == '\0';
}
}
immutable nullChar = '\0';
@safe unittest
{
import std.array : staticArray;
auto localStr = "你好啊!".staticArray;
auto localCStr = localStr.CString;
assert(localCStr.front == 'h');
static immutable(char)* staticPtr;
// 错误,逃逸本地引用
// staticPtr = &localCStr.front();
// 好.
staticPtr = &CString("全局\0").front();
localCStr.popFront;
assert(localCStr.front == 'e');
assert(!localCStr.empty);
}
第一部分说@trusted对DIP1000,是把可怕脚枪.该示例说明了原因.使用普通断定或完全忘记它们是多么容易,或忽略使用匿名联合的需要.认为可安全使用该结构,但完全忽略了一些东西.
域不仅用于注释参数和局部变量.还可用于域类和域保护语句.已弃用域类,而域保护与DIP1000或变量寿命控制无关.
中/中引用/中域还有个意思:
@safe void getFirstSpace
(
ref scope string result,
return scope string where
)
{
//...
}
return属性的一般含义在此无意义,因为函数为void返回类型.这里有个特殊规则:如果返回类型是void,且第一个参数是ref/out,则假定后续的中(引用/域)通过赋值给第一个参数来逃逸.对结构成员函数,假定赋值为结构自身.
@safe unittest
{
static string output;
immutable(char)[8] input = "栈上";
//试赋值给栈变量,不会编译
getFirstSpace(output, input);
}
既然有了out,对result而言,out比ref好,out与ref差不多,唯一区别是在函数开始时自动默认初化引用数据,表明out参数所引用数据保证不会影响函数.
编译器用域在函数体内优化类分配.如果用new类,初化域变量,编译器把它放在栈中.示例:
class C{int a, b, c;}
@safe @nogc unittest
{
// 单元测试为@nogc,
// 如无域优化,则不编译
scope C c = new C();
}
需要显式使用域关键字.推导域不管用,因为这样初化类一般不会(无@nogc属性)强制限制c.该功能目前仅适合类,但也可与新建的结构指针和数组字面一起使用.
这,基本上就是dip1000的手册了,
下节讲推导属性,也与dip1000有关.还会涵盖一些敢于使用@trusted和@system编码时的注意事项.存在危险系统编程需求,D也可减小风险.
浙公网安备 33010602011771号