在学习D语言的过程中,我试图制作一个通用的Matrix类,以支持所包含对象的类型提升。
也就是说,当我将a乘以a时Matrix!(int)
,Matrix!(real)
我应该得到Matrix!(real)
一个结果。
由于类型升级有很多不同的类型,因此opBinary
为每种可能的组合重新实现该方法将非常繁琐,并且会产生大量样板代码。因此,mixin / mixin模板似乎是答案。
我无法理解的是为什么第一个代码示例有效
import std.stdio;
import std.string : format;
string define_opbinary(string other_type) {
return "
Matrix opBinary(string op)(Matrix!(%s) other) {
if(op == \"*\") {
Matrix result;
if(this.columns == other.rows) {
result = new Matrix(this.rows, other.columns);
} else {
result = new Matrix(0,0);
}
return result;
} else assert(0, \"Operator \"~op~\" not implemented\");
}
".format(other_type);
}
class Matrix(T) {
T[][] storage;
size_t rows;
size_t columns;
const string type = T.stringof;
this(size_t rows, size_t columns) {
this.storage = new T[][](rows, columns);
this.rows = rows;
this.columns = columns;
}
void opIndexAssign(T value, size_t row, size_t column) {
storage[row][column] = value;
}
mixin(define_opbinary(int.stringof));
mixin(define_opbinary(uint.stringof));
}
void main()
{
Matrix!int mymat = new Matrix!(int)(2, 2);
mymat[0,0] = 5;
writeln(mymat.type);
Matrix!uint mymat2 = new Matrix!(uint)(2, 2);
writeln(mymat2.type);
auto result = mymat * mymat2;
writeln("result.rows=", result.rows);
writeln("result.columns=", result.columns);
auto result2 = mymat2 * mymat;
writeln("result.type=",result.type);
writeln("result2.type=",result2.type);
}
Run Code Online (Sandbox Code Playgroud)
配音输出:
Performing "debug" build using /usr/bin/dmd for x86_64.
matrix ~master: building configuration "application"...
Linking...
Running ./matrix.exe
50
00
int
uint
result.rows=2
result.columns=2
00
00
result.type=int
result2.type=uint
Run Code Online (Sandbox Code Playgroud)
但是第二个代码示例不起作用
import std.stdio;
import std.string : format;
mixin template define_opbinary(alias other_type) {
Matrix opBinary(string op)(Matrix!(other_type) other) {
if(op == "*") {
Matrix result;
if(this.columns == other.rows) {
result = new Matrix(this.rows, other.columns);
} else {
result = new Matrix(0,0);
}
return result;
} else assert(0, "Operator "~op~" not implemented");
}
}
class Matrix(T) {
T[][] storage;
size_t rows;
size_t columns;
const string type = T.stringof;
this(size_t rows, size_t columns) {
this.storage = new T[][](rows, columns);
this.rows = rows;
this.columns = columns;
}
void opIndexAssign(T value, size_t row, size_t column) {
storage[row][column] = value;
}
mixin define_opbinary!(int);
mixin define_opbinary!(uint);
}
void main()
{
Matrix!int mymat = new Matrix!(int)(2, 2);
mymat[0,0] = 5;
writeln(mymat.type);
Matrix!uint mymat2 = new Matrix!(uint)(2, 2);
writeln(mymat2.type);
auto result = mymat * mymat2;
writeln("result.rows=", result.rows);
writeln("result.columns=", result.columns);
auto result2 = mymat2 * mymat;
writeln("result.type=",result.type);
writeln("result2.type=",result2.type);
}
Run Code Online (Sandbox Code Playgroud)
配音输出:
source/app.d(60,19): Error: cast(Object)mymat is not of arithmetic type, it is a object.Object
source/app.d(60,27): Error: cast(Object)mymat2 is not of arithmetic type, it is a object.Object
source/app.d(64,20): Error: cast(Object)mymat2 is not of arithmetic type, it is a object.Object
source/app.d(64,29): Error: cast(Object)mymat is not of arithmetic type, it is a object.Object
/usr/bin/dmd failed with exit code 1.
Run Code Online (Sandbox Code Playgroud)
极为奇怪的是,如果我删除mixin define_opbinary!(int);
呼叫,那么我只会收到两个算术投诉(仅剩下关于行60(auto result = mymat * mymat2;
)的两个投诉)。
我有一种感觉,编译器以某种方式认为这两个mixin调用是模棱两可的,并删除了两者,但我不确定。
任何帮助将不胜感激。
哦,我对此有很多话要说,包括我不会为此使用任何一种mixin-我只会使用普通的模板。最后,我会再说。
我将尝试变得相当全面,因此,如果我描述您已经知道的内容,则表示歉意;另一方面,为了提供更深入的理解,我可能还会提供一些无关紧要的材料。
首先,mixin与模板mixin。混入()采用一个串,将其解析为一个AST节点(AST顺便说一句是用于表示代码编译器的内部数据结构,它代表“抽象语法树。” foo()
是像一个AST节点FunctionCall { args: [] }
。if(foo) {}
是一个像IfStatement { condition: Expression { arg: Variable { name: foo }, body : EmptyStatement }
-基本上对象表示每个代码的一部分)。
然后,它将已解析的AST节点粘贴到mixin
单词出现的相同插槽中。您通常可以将其视为复制/粘贴代码字符串,但是受限于此字符串必须表示完整元素,并且在混入没有错误的相同上下文中必须可以替换。因此,就像您无法int a = bmixin(c)
在变量b
前添加变量一样,mixin必须自己表示一个完整的节点。
但是,一旦将其粘贴到该AST节点中,编译器就将其视为原始代码全部写入那里。所引用的任何名称都将在粘贴的上下文中进行查找,等等。
另一方面,模板mixin实际上在AST中仍具有一个容器元素,该容器元素用于名称查找。实际上,它的工作方式类似于struct
或class
在编译器内部的工作方式-它们都具有一个子声明列表,这些子声明作为一个单元保留在一起。
最大的区别在于,通常可以从父上下文中自动访问模板mixin的内容。它遵循类似于类继承的规则,在其中class Foo : Bar
可以将Bar的成员视为自己的成员,但是它们仍然保持独立。您仍然可以喜欢super.method();
并调用它,而不必考虑孩子的替代项。
“通常”是由于过载和劫持规则而出现的。深入了解和原理:https://dlang.org/articles/hijack.html
但这是为了防止第三方代码在添加新功能时默默地更改程序的行为,D要求程序员在使用时将所有函数重载集合合并在一起,并且对操作员重载特别挑剔,因为它们已经具有默认行为,即任何mixin都会被修改。
mixin template B(T) {
void foo(T t) {}
}
class A {
mixin B!int;
mixin B!string;
}
Run Code Online (Sandbox Code Playgroud)
这类似于您拥有的代码,但是具有普通功能。如果编译并运行,它将起作用。现在,让我们直接向A添加foo重载:
mixin template B(T) {
void foo(T t) {}
}
class A {
mixin B!int;
mixin B!string;
void foo(float t) {}
}
Run Code Online (Sandbox Code Playgroud)
如果尝试使用字符串参数进行编译,则它实际上会失败!“错误:使用参数类型(字符串)无法调用函数poi.A.foo(float t)”。为什么不使用mixin?
这是模板混合的规则-请记住,编译器仍将它们视为一个单元,而不仅仅是粘贴的声明集。外部对象上存在的任何名称(这里是我们的类A
)都将被使用,而不是在模板mixin中查找。
因此,它看到A.foo
并且不会费心寻找B
一个foo。这对于从模板mixin覆盖特定内容很有用,但是在尝试添加重载时可能会很麻烦。解决方案是alias
在顶层添加一行,以告诉编译器专门查看内部。首先,我们需要给mixin一个名称,然后明确转发该名称:
mixin template B(T) {
void foo(T t) {}
}
class A {
mixin B!int bint; // added a name here
mixin B!string bstring; // and here
alias foo = bint.foo; // forward foo to the template mixin
alias foo = bstring.foo; // and this one too
void foo(float t) {}
}
void main() {
A a = new A;
a.foo("a");
}
Run Code Online (Sandbox Code Playgroud)
现在,它适用于float,int和string ....,但它也有点违反了模板mixins添加重载的目的。您可以采取的一项技巧是将顶级模板函数放入中A
,然后将其转发给mixin ...,只是它们需要使用不同的名称进行注册。
这使我回到了您的代码。就像我说的那样,D对于运算符重载特别挑剔,因为它们总是会覆盖正常行为(即使该正常行为是错误的,例如在类中)。您需要在顶层明确说明它们。
考虑以下:
import std.stdio;
import std.string : format;
mixin template define_opbinary(alias other_type) {
// I renamed this to opBinaryHelper since it will not be used directly
// but rather called from the top level
Matrix opBinaryHelper(string op)(Matrix!(other_type) other) {
if(op == "*") {
Matrix result;
if(this.columns == other.rows) {
result = new Matrix(this.rows, other.columns);
} else {
result = new Matrix(0,0);
}
return result;
} else assert(0, "Operator "~op~" not implemented");
}
}
class Matrix(T) {
T[][] storage;
size_t rows;
size_t columns;
const string type = T.stringof;
this(size_t rows, size_t columns) {
this.storage = new T[][](rows, columns);
this.rows = rows;
this.columns = columns;
}
void opIndexAssign(T value, size_t row, size_t column) {
storage[row][column] = value;
}
mixin define_opbinary!(int);
mixin define_opbinary!(uint);
// and now here, we do a top-level opBinary that calls the helper
auto opBinary(string op, M)(M rhs) {
return this.opBinaryHelper!(op)(rhs);
}
}
void main()
{
Matrix!int mymat = new Matrix!(int)(2, 2);
mymat[0,0] = 5;
writeln(mymat.type);
Matrix!uint mymat2 = new Matrix!(uint)(2, 2);
writeln(mymat2.type);
auto result = mymat * mymat2;
writeln("result.rows=", result.rows);
writeln("result.columns=", result.columns);
auto result2 = mymat2 * mymat;
writeln("result.type=",result.type);
writeln("result2.type=",result2.type);
}
Run Code Online (Sandbox Code Playgroud)
我粘贴了完整的代码,但实际上只有两个更改:mixin模板现在定义了一个具有不同名称(opBinaryHelper
)的帮助程序,并且顶层类现在具有opBinary
转发至该帮助程序的显式定义。(顺便说一句,如果要添加其他重载,则alias
上面的技巧可能是必要的,但是在这种情况下,由于所有重载都是if
从同一个名称中调度的,因此可以自动合并所有帮助程序。)
最后,代码可以正常工作。
现在,为什么对于字符串mixin不需要任何这些操作?好了,回到原始定义:一个字符串mixin对其进行解析,然后将其粘贴到AST节点中,就好像它最初是在其中写入的一样。后者使它可以工作(一旦混合了一个字符串,您就被它困住了,因此,如果您不喜欢它的一部分,则必须修改库,而不仅仅是覆盖一部分)。
模板mixin维护自己的子命名空间,以允许选择性覆盖等,并且使用这些更严格的重载规则触发犯规。
最后,这是我实际执行的方式:
// this MatrixType : stuff magic means to accept any Matrix, and extract
// the other type out of it.
// a little docs: https://dlang.org/spec/template.html#alias_parameter_specialization
// basically, write a pattern that represents the type, then comma-separate
// a list of placeholders you declared in that pattern
auto opBinary(string op, MatrixType : Matrix!Other_Type, Other_Type)(MatrixType other) {
// let the compiler do the promotion work for us!
// we just fetch the type of regular multiplication between the two types
// the .init just uses the initial default value of the types as a placeholder,
// all we really care about is the type, just can't multiply types, only
// values hence using that.
alias PromotedType = typeof(T.init * Other_Type.init);
// in your version, you used `if`, but since this is a compile-time
// parameter, we can use `static if` instead and get more flexibility
// on stuff like actually changing the return value per operation.
//
// Don't need it here, but wanted to point it out anyway.
static if(op == "*") {
// and now use that type for the result
Matrix!PromotedType result;
if(this.columns == other.rows) {
result = new Matrix!PromotedType(this.rows, other.columns);
} else {
result = new Matrix!PromotedType(0,0);
}
return result;
// and with static if, we can static assert to turn that runtime
// exception into a compile-time error
} else static assert(0, "Operator "~op~" not implemented");
}
Run Code Online (Sandbox Code Playgroud)
只需将opBinary放到您的类中,现在一个函数就可以处理所有情况-无需列出特定类型,因此完全不再需要mixin魔术!(....好吧,除非您需要使用子类进行虚拟覆盖,但这是另一个主题。简短的提示,这是可能的static foreach
,我在我上一个SO答案中谈到过:https : //stackoverflow.com / a / 57599398/1457000)
在这个小函数中有一些D技巧,但是我尝试在代码的注释中进行解释。不过,请随时询问您是否需要进一步说明- : patterns
模板中的内容是IMO较高级的D编译时反射内容之一,因此一开始并不容易,但是对于像这样的简单情况,这很有意义,只需将其视为带有占位符的声明即可。
归档时间: |
|
查看次数: |
140 次 |
最近记录: |