admin 管理员组

文章数量: 887053

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.saySomethingDuck.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中使用structsunion.引用structunion时,工作方式与引用其他类型时相同.但是在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保证相同.但同时,因为arrref参数,mergeSort可改变传递给它的数组.然后客户可写:

float[] values = [5, 1.5, 0, 19, 1.5, 1];
values.mergeSort;

用非ref参数,客户就要values=values.sort(虽然这里不用引用是合理的).而指针无法这样,因为中域float[]*arr会保护数组元数据地址(数组的lengthptr字段),而不是内容地址.
也可给域引用提供可返回的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

一般需要仔细考虑,refreturn ref来跟踪保护哪个地址可返回.熟悉后,理解structsunions如何同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);
}

第一部分说@trustedDIP1000,是把可怕脚枪.该示例说明了原因.使用普通断定或完全忘记它们是多么容易,或忽略使用匿名联合的需要.认为可安全使用该结构,但完全忽略了一些东西.

不仅用于注释参数和局部变量.还可用于域类域保护语句.已弃用域类,而域保护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而言,outref好,outref差不多,唯一区别是在函数开始时自动默认初化引用数据,表明out参数所引用数据保证不会影响函数.
编译器用在函数体内优化类分配.如果用new类,初化变量,编译器把它放在栈中.示例:

class C{int a, b, c;}
@safe @nogc unittest
{// 单元测试为@nogc,// 如无域优化,则不编译scope C c = new C();
}

需要显式使用关键字.推导不管用,因为这样初化类一般不会(无@nogc属性)强制限制c.该功能目前仅适合,但也可与新建的结构指针和数组字面一起使用.
这,基本上就是dip1000的手册了,
下节推导属性,也与dip1000有关.还会涵盖一些敢于使用@trusted@system编码时的注意事项.存在危险系统编程需求,D也可减小风险.

本文标签: dip1000 2