以下大部分是正确的;我可能错过了什么或误解了什么;阅读链接的文档(它太短了)并在评论中解雇 - 谢谢。
不好比喻: 一个类是一个千篇一律的东西。 一个实例是由曲奇切割器制成的曲奇。
类比的问题: 我也打算打电话给JSL的班级一种原型. 原型是实际的 cookie。 原型饼干可以被吃掉或被修改,但你通常不想这样做。 相反,您应该将其轮廓追踪到饼干面团中,然后用剪刀将其剪开以制作新的饼干实例。 但是如果你真的咬了一口原型 cookie,所有未来的 cookie 实例看起来也会被咬(第二个例子)。 此外,原型 cookie 不运行 _init_(...) 方法,因此它可能是半生不熟的,但它具有正确的形状。
这文档使用模板而不是原型. 我更喜欢原型而不是模板;原型是一个可用的对象,而模板只是显示喷漆应该去的地方。 对于第一个示例,这种微妙的区别并不重要。
一个简单的例子
这是一个简单的单类示例气泡. 类原型由 DefineClass() 制作。 NewObject() 使得来自原型的 61 个实际实例。 61 个实例中的每一个都有一个半径和一个 x,y 中心。 该原型还提供了 _init_(...)、drift() 和 draw() 方法。 使用对象显示图形每个对象的绘制方法。 然后循环运行,为每个对象调用对象漂移方法,然后告诉图形重新绘制。 结果是这样的动画图:
图形的 JSL:
// define a simple class (like a template) to represent a 2D bubble
Define Class("bubble",
// class member variables
m_radius = .; // class member variables are specified in
m_xpos = .; // the class definition with initial values.
m_ypos = .; // these values are copied into instances created by NewObject(...)
// class methods
_init_ = Method( {radius, x, y}, // called by NewObject(bubble(r,x,y)) below to initialize an instance
this:m_xpos = x; // this: is the namespace of an instance
m_ypos = y; // you don't need "this:" unless the member name is
m_radius = radius; // the same as a parameter name
);
drift = Method( {}, // a method to move the bubble
m_xpos = m_xpos * 0.995;// tentatively drift to center
m_ypos = m_ypos * 0.995;
);
draw = Method( {}, // a method to draw the bubble
Circle( {m_xpos, m_ypos}, m_radius )
);
);
// make a bunch of instances of the bubble class, keep them in a list
bubblelist = {};
For( i = 1, i <= 61, i += 1,
Insert Into( bubblelist,
// each instance of bubble is initialized with a random radius and position
New Object( bubble( Random Uniform( .03, .8 ), Random Uniform( -.9, .9 ), Random Uniform( -.9, .9 ) ) )
)
);
picno = 0; // need an ID for each picture
// make a window to display all the instances of the bubble class
New Window( "bubble class instances",
gb = Graph Box( // the graphic script for the graph box scales and sizes the axes and...
xaxis( {Add Ref Line( 0, "Solid", "Black", "", 1 )} ),
yaxis( {Add Ref Line( 0, "Solid", "Black", "", 1 )} ),
framesize( 600, 600 ),
X Scale( -1, 1 ),
Y Scale( -1, 1 ),
// the graphic script tells each bubble to draw itself
For( i = 1, i <= N Items( bubblelist ), i += 1,
bubblelist[i]:draw()
);
text({-.9,-.9},char(picno)),
<<ONCLOSE></ONCLOSE>我对其工作原理的心智模型:在幕后,DefineClass() 保留了所有 DefineClass() 原型的隐藏列表,它们看起来很多像实例。 稍后会详细介绍;上面的示例通过 NewObject() 函数访问它们。 NewObject() 克隆原型对象的新实例并运行 _init_ 方法。
什么是这个: ?
在上面的示例中,我将成员变量命名为米_字首。 这不是必需的,但它对于记住哪些变量是成员变量以及防止与参数名称和局部变量名称发生冲突很有用。 不过我也用过这个:m_xpos在 _init_() 方法分配之一上。 这这个:命名空间的意思是“当前实例的成员变量”。 所以这个:x = x;如果您希望成员变量 x 和参数 x 具有相同的名称,则可以使用。米_对于维护您的代码的下一个人来说会更容易。
什么是气泡: ?
还有另一个命名空间你通常不需要这将说明为什么我说隐藏列表中的 DefineClass() 原型看起来很多like instances——类名是一个命名空间。 使用气泡:m_radius将访问原型的 m_radius,即使是从实例方法。 如果你认为你需要一个静态成员变量,这是一种制作方法……如果你小心的话。
使用静态值的原型
下一个示例使用静态 m_ID 变量。 正如评论所暗示的那样,我可能应该做出不同的变化,但这个变化仍然有用,因为它解释了“如果你小心”警告。 这是发生的事情(再次是我的心智模型):
当下面的 _init_() 运行时,bubble2 类已经将所有原型内容复制到新实例中。 新实例具有原型的 m_id 的副本。 _init_() 然后更改原型中的 m_id()。 下一个实例获取更改后的值。 其他成员函数必须在原型中的 m_id (bubble2:m_id) 和 this:m_id(或者只是没有命名空间的 m_id)之间进行选择。 在这里,他们使用 m_id,它是实例唯一整数的副本。
在视频中,最后一块拼图在 5 点 15 分落在屏幕底部边缘。 黑色圆圈是显示其 m_id 值的 bubble2。 他们使用 m_id 来确保在研究所有近邻时忽略自己。
// twist is a scaling divisor for a shearing rotation that moves
// the outer edge faster.
// this value needs to get bigger with more beehive cells.
global:twist = 600;
// choose the size to make them fit, but it also interacts with twist
// because the distance from the center grows
global:bubblesize = 0.057;
// use a defined class to represent each cell in the beehive.
Define Class("bubble2",
m_radius = .; // in the video, all beehive cells have the same radius
m_xpos = .; // the beehive cell's position
m_ypos = .;
//
// this is answering the question "can a JSL Class have a static member variable?"
// this could have been done more intuitively for this example by passing the
// index of the creation loop to the _init_ code, but here it is...initialize the
// "static" value to 1...
m_id = 1; // keep reading to understand the two-faced nature of this variable...
// What actually happens: when newObject(bubble2(...)) makes a new instance, the _init_(...)
// is called.
// First, the m_id is cloned from the prototype to the instance. The first instance gets m_id==1.
// Second, the bubble2: namespace causes the prototype class to increment the
// prototype's m_id. The next instance gets the new value from the prototype, m_id==2.
// this is a little odd, coming from a C++ -like language; in the drift method below,
// "other:m_id != m_id" both m_id values are regular non-static instance members.
// if you want the static behaviour, use the bubble2:m_id approach (bubble2 is the class
// name and access the singleton in the prototype class.)
// Normally you *never* want to use the class name as a name space qualifier because
// you set up the class correctly and initialize via _init_( parameters ). And normally
// a static value is also a constant value, so using a copy is just fine. But if
// multiple instances need to communicate via a singleton static, AND you always use
// the namespace, this is a reasonable place to keep the static.
// Having written all that, I now dislike this particular example because it is using
// a confusing combination of bubble2:m_id and instance values of m_id.
_init_ = Method( {radius, x, y},
m_radius = radius;
this:m_xpos = x; // this: (or nothing) operates on an instance
m_ypos = y;
bubble2:m_id++; // bubble2: operates on the prototype. The next newobject(bubble2(...)) uses the modified prototype
);
distanceBetweenCenters = Method( {other},
/*return*/ Sqrt( (m_xpos - other:m_xpos) ^ 2 + (m_ypos - other:m_ypos) ^ 2 )
);
distanceBetweenEdges = Method( {other}, // negative: overlaps
/*return*/ distanceBetweenCenters( other ) - (m_radius + other:m_radius)
);
drift = Method( {},
// bubbles should pull to 0,0 AND move away from near bubbles
angle = ATan( m_ypos, m_xpos );
radius = Sqrt( m_ypos ^ 2 + m_xpos ^ 2 );
shear = global:twist; // also depends on number of circles
shrink = .99950;
// radius/shear creates a tiny amount of rotation, more on the edges. That rotational
// shearing helps the circles bounce about. /400 is not enough and /100 is too much.
// radius*shrink pulls all the circles to the origin. It is balanced by push, below.
x = Cos( angle + radius / shear ) * radius * shrink;
y = Sin( angle + radius / shear ) * radius * shrink;
// check neighbors
{neighbors, distances} = kdt << kNearestRows( 7, m_id );
tension = 0;
ntension = 0; // calculate a tension, or stress, for contour to use as a color
For( iN = 1, iN <= N Items( neighbors ), iN += 1,
i = neighbors[iN];
other = bubblelist[i];
If( other:m_id != m_id, // <<<<<<<<<<<<<<<< NOT the bubble2:m_id value !!!
dist = distanceBetweenEdges( other );
// if the dist is zero, it is perfect. if the other guy is far away, ignore him.
If( Abs( dist ) < global:bubblesize * 0.8,
tension += dist ^ 2; // this will be a RMS value at the bottom
ntension += 1;
);
If( dist < 0, //push
angle = ATan( m_ypos - other:m_ypos, m_xpos - other:m_xpos );
x += Cos( angle ) * Abs( dist ) ^ .4 * .05;
y += Sin( angle ) * Abs( dist ) ^ .4 * .05;
);//
, //
Beep()
);
);
m_xpos = x;
m_ypos = y;
If( ntension == 0,
ntension = 1;
tension += (global:bubblesize / 2) ^ 2;
);
Sqrt( tension / ntension ); // RMS
);
draw = Method( {},
Transparency( .8 );
Circle( {m_xpos, m_ypos}, m_radius, "FILL" );
Transparency( 1 );
Text( center justified, {m_xpos, m_ypos - m_radius * .2}, Char( m_id ) );
);
);
bubblelist = {};
n = 12; // how many "rings" of hex cells
m = -26 + 21 * n + 3 * (n - 3) ^ 2; // yes, I used JMP to get this formula
For( i = 1, i <= m, i += 1,
Insert Into(
bubblelist,
New Object( bubble2( Random Uniform( global:bubblesize, global:bubblesize ), Random Uniform( -.9, .9 ), Random Uniform( -.9, .9 ) ) )
)
);
// need a table for contour plot, and it will include 100 values in a circle around the hive
ring = 100;
dt = New Table( "tension",
Add Rows( m + ring ),
New Column( "x", Numeric, "Continuous", Format( "Best", 12 ), Set Values( [] ) ),
New Column( "y", Numeric, "Continuous", Format( "Best", 12 ), Set Values( [] ) ),
New Column( "tension", Numeric, "Continuous", Format( "Best", 12 ), Set Values( [] ) )
);
For( i = 1, i <= ring, i += 1,
angle = 2 * Pi() * i / ring;
dt:x[m + i] = 1.25 * Sin( angle );
dt:y[m + i] = 1.25 * Cos( angle );
dt:tension[m + i] = 0;
);
cplot = dt << Contour Plot(
X( :x, :y ),
Y( :tension ),
Show Data Points( 0 ),
Fill Areas( 1 ),
Label Contours( 0 ),
Transform( "Range Normalized" ),
Color Theme( "Green Yellow Red" ),
Specify Contours( Min( 0 ), Max( .01 ), N( 50 ) )
);
Report( cplot )[framebox( 1 )] << Frame Size( 1920, 1080 ) << maximizewindow( 1 ) << backgroundcolor( "black" );
Report( cplot )[framebox( 1 )] << xaxis(
{Format( "Fixed Dec", 12, 3 ), Min( -1.4*1920/1080 ), Max( 1.4*1920/1080 ), Inc( 0.5 ), Label Row(
{Show Major Grid( 0 ), Show Major Labels( 0 ), Show Major Ticks( 0 ), Show Minor Grid( 0 ), Show Minor Labels( 0 ), Show Minor Ticks( 0 )}
)}
);
Report( cplot )[framebox( 1 )] << yaxis(
{Format( "Fixed Dec", 12, 3 ), Min( -1.4 ), Max( 1.4 ), Inc( 0.5 ), Label Row(
{Show Major Grid( 0 ), Show Major Labels( 0 ), Show Major Ticks( 0 ), Show Minor Grid( 0 ), Show Minor Labels( 0 ), Show Minor Ticks( 0 )}
)}
);
// here is the script that makes the bubbles draw
Report( cplot )[framebox( 1 )] << addgraphicsscript(
Fill Color( "black" );
text Color( "white" );
pen Color( rgbcolor(64,64,64) );
For( i = 1, i <= N Items( bubblelist ), i += 1,
bubblelist[i]:draw()
);
hline(0);
vline(0);
);
cplot << onclose(
keepRunning = 0;
Close( dt, nosave );
1;
);
Wait( 0 );
//dir = "$temp\ClassWithStatic" || Char( global:twist ) || "\";
//Delete Directory( dir );
//Create Directory( dir );
//Write( Convert File Path( dir, "windows" ) );
picno = 0;
keepRunning = 1;
While( keepRunning,
xy = J( N Items( bubblelist ), 2, . );
For( i = 1, i <= N Items( bubblelist ), i += 1,
xy[i, 1] = bubblelist[i]:m_xpos;
xy[i, 2] = bubblelist[i]:m_ypos;
);
// the kdtable makes it faster to find near neighbors
kdt = KDTable( xy );
For( i = 1, i <= N Items( bubblelist ), i += 1,
dt:tension[i] = bubblelist[i]:drift();
dt:x[i] = bubblelist[i]:m_xpos;
dt:y[i] = bubblelist[i]:m_ypos;
);
Report( cplot )[framebox( 1 )] << inval << updatewindow;
picno += 1;
// Report( cplot )[framebox( 1 )] << save picture( dir || Char( (picno) + 1e6 ) || ".png" );
Wait( .01 );
);
上面的两个例子都可以很容易地(也许更容易)用数据表的行来表示对象,用列来表示成员变量和方法的 JSL 用户函数。 使用类,尤其是没有任何子类化的类,如果其他人对 JSL 中的类不满意,可能会使您的 JSL 更难遵循。
说到函数,DefineClass 的文档也提到了它们。 据我所知,它们几乎没有带来任何好处,应该避免。 我认为类的每个实例都可能获得每个方法和每个成员变量的单独副本。 如果您定义一个 Function() 并将其存储在一个成员变量中,那么您只是在制作其他东西来复制(可能)可能已经在一个更全局的名称空间中只有一个副本。 但是,如果只有一个(或很少的)实例,并且您确实需要一个函数,该类除了其参数外什么都不知道,那么您可能需要一个类中的函数。
继续最后一个例子,让我们做一些子类化并查看最后一个命名空间,极好的: ,我必须寻找它(请参阅 JSL 中的评论)。
直观的例子:基类可能是车辆,子类(或派生类)是卡车和汽车。 一辆卡车可能会用知道避开居民区街道的方法代替基类导航方法。 汽车可能会利用 HOV 车道。 当被告知从 A 导航到 B 时,一系列车辆都会做正确的事情。
Define Class 有一个有趣的论点:基类(...) . 在下面的示例中,形状将是基类和正方形和圆圈将从形状中导出。 该示例生成一个正方形和圆形列表并显示它们。
two squares, two circles, two outlined, two filled两个正方形,两个圆圈,两个轮廓,两个填充
以下描述基于反复试验;我相信它的正确性足以成为一个有用的模型来预测某事将如何运作。
什么是极好的: ?
创建派生类时,JMP 运行到派生类的末尾基类链并回到顶部,首先创建基类,并在它们自己的名称空间中。 命名空间在内部链接在一起,因此派生类可以通过查看自己的命名空间来引用成员变量,然后沿着命名空间链向下找到基类,直到找到该变量。 这极好的: 命名空间只是意味着在命名空间链中向前跳一次而不查看当前命名空间。 这就是为什么下面的 JSL 说“如果不需要,不要使用 super: 调用基方法”;基方法将无法访问派生命名空间,因此它不能调用派生函数。
多重继承:如果您使用“接口”并避免重名,它可能会起作用。 祝你好运。 我不确定同级基类在上面模型中的名称空间链中的位置。
// demonstrates JMP class inheritance and overriding a method
// and calling the super class. A circle and a square class
// are derived from a shape class. Explains when to NOT use super:.
//
// I found super: in https://github.com/sassoftware/jsl-hamcrest/blob/master/Source/Core/Matchers/EqualTo.jsl
// it isn't documented anywhere else, but works great for calling base class _init_
// Also, https://github.com/sassoftware/jsl-hamcrest/blob/master/Source/Core/Utils.jsl
// shows a "self" implementation idea. This is a bad idea because it creates a circular reference
// that must be cleared before the object is lost/deleted...or it will leak.
// avoid making members and methods in base and derived classes with the same name.
// _init_, _to string_, _show_ are exceptions, but they must not try to use a derived
// class member or method. (you would not normally want to, so no problem.)
//
// avoid needless upcast (super:); it cripples the base class because there is no downcast or
// virtual mechanism. Without the upcast, a baseclass function runs with access to the
// derived class members and methods. (see square's draw method calling docolor calling record.)
//
// it appears that the derived chain is searched *after* the current class; in a two class
// (base,derived) example that inverts the search order for members and methods when a
// derived class uses "super:" to call a base class method. If the base class calls a "virtual"
// function that exists in both base and derived, it won't get the derived one if "super:" was
// used but will if no scope is used. You only need "super:" typically when you want to call
// the base class method from an overridden method of the same name. If the base class method
// isn't calling back to derived via a virtual, everything is good.
// deep details
//
// the output shows lines like this:
// record square=square(shape=(s= x=30 y=30 r=5 s=4 c=yellow f=1))
// record circle=circle(shape=(s= x=30 y=60 r=10 s=99 c=green f=0))
//
// the square and the circle both get the right answer via different
// paths. Because of way draw() is overridden by square and not by circle,
// the initial call, objects[window:i] << draw(), is calling either a
// derived member or a base member. In both cases, "this" is for the
// derived member. in square's draw(), I made an error, now commented
// out: /*super:*/docolor(), that caused docolor to run with "this"
// for the base class, not the derived class. docolor then calls record().
// all three classes have record() members. The intent is to override the
// base class version ("never seen"). The error caused the base class
// version to run for square because "this" of docolor() was for the base class.
Define Class( "shape", // base class for both shapes
m_Xcenter = .;
m_Ycenter = .;
m_radius = .;
m_sides = .;
m_color = .;
m_filled = .;
_init_ = Method( {x, y, radius, sides, color, fill},
m_Xcenter = x;
m_Ycenter = y;
m_radius = radius;
m_sides = sides;
m_color = color;
m_filled = fill;
);
_to string_ = Method( {},
"shape=(s=" ||
" x=" || Char( m_Xcenter ) || " y=" || Char( m_Ycenter ) ||
" r=" || Char( m_radius ) || " s=" || Char( m_sides ) ||
" c=" || Char( m_color ) || " f=" || Char( m_filled ) || ")"
);
_show_ = _to string_;
// the following should NOT ever get called because square and circle override it
// try changing square to call super:docolor and then you'll see this
record=method({},show("never seen, overridden by square and circle"));
// both shapes understand filled vs outlined
// see note in square:draw method about calling docolor without super:
docolor = Method( {},
this:record(); // this: doesn't change anything,
If( this:m_filled, // it is mostly for disambiguating
Fill Color( /*this:*/m_color );// parameter of same name
, //
Pen Color( this:m_color )
)
);
// the base class has a draw method that circle uses;
// square provides an override because this draws a
// diamond-oriented square
draw = Method( {},
docolor();
p = J( m_sides, 3, 2 );//2==draw
p[1, 3] = 1;//1==move
p[m_sides, 3] = -2;//-2==draw and close
For( i = 1, i <= m_sides, i += 1,
p[i, 1] = m_Xcenter + m_radius * Cos( 2 * Pi() * i / m_sides );
p[i, 2] = m_Ycenter + m_radius * Sin( 2 * Pi() * i / m_sides );
);
Path( p, m_filled );
);
);
Define Class( "circle", baseclass( shape ),
_init_ = Method( {x, y, radius, color, fill},
super:_init_( x, y, radius, 99/*sides*/, color, fill )
);
_to string_ = Method( {},
"circle(" || super:_tostring_() || ")"
);
_show_ = _to string_;
record=method({},write("\!nrecord circle=",_show_()));
// circle has no override for draw()
);
Define Class( "square", baseclass( shape ),
_init_ = Method( {x, y, radius, color, fill},
super:_init_( x, y, radius, 4/*sides*/, color, fill )
);
_to string_ = Method( {},
"square(" || super:_tostring_() || ")"
);
_show_ = _to string_;
record=method({},write("\!nrecord square=",_show_()));
// square overrides the base class draw
draw = Method( {},
/*super:*/docolor();// don't up-cast, docolor can't down-cast and gets base class record()
Rect(
m_Xcenter - m_radius,
m_Ycenter - m_radius,
m_Xcenter + m_radius,
m_Ycenter + m_radius,
m_filled
);
);
);
objects = {};
Insert Into( objects, New Object( square( 30, 30, 5, "yellow", 1 ) ) );
Insert Into( objects, New Object( Circle( 30, 60, 10, "green", 0 ) ) );
Insert Into( objects, New Object( square( 60, 30, 15, "blue", 0 ) ) );
Insert Into( objects, New Object( Circle( 60, 60, 20, "red", 1 ) ) );
New Window( "objects",
gb = Graph Box( framesize( 500, 500 ),
For( window:i = 1, window:i <= N Items( objects ), window:i += 1,
//Show( objects[i] );
objects[window:i] << draw(); // uses the base class or the derived class draw
)
)
);
//stop();
// animate the color of object 4, the big red circle
For( i = 0, i <= 1, i += (1 / 256), //show(i);
objects[4]:m_color = HLS Color( i, .5, 1 );
gb << inval << updatewindow;
Wait( 0 );
);
关于什么这个?
C++ 有一个名为“this”的值(其他语言使用“self”和“me”),对象有时需要它来引用自身。 与此不同:名称空间前缀。 我不认为 JSL 类在类中提供了一个“this”,你可能会想做这样一个:
x = newObject(myClass(17));
x::self = x;
或类似的东西。 然后 myClass 方法可以使用“self”来做需要类实例的事情,例如克隆。
Scripting index showing messages that can be sent to a class instance显示可以发送到类实例的消息的脚本索引
在包含(指向)类实例的类实例中保留成员变量会造成内存泄漏。 这给我们带来了一个新话题……实际上是话题。
什么时候删除实例?
当没有对实例的引用时,实例将被删除。 在上面的部分中,实例中的“self”引用将防止实例被删除,即使您丢失了实例的所有句柄. 除了关闭 JMP 之外,没有办法清理它。
什么时候复制实例?
这是最好的部分:仅当您使用 <这意味着实例通过引用传递。耶! 创建一个简单的类来包装一个大矩阵并将该类的一个实例传递给用户函数,而不是让 JMP 将矩阵参数复制到参数值中,并可能返回另一个副本,这样就不那么笨拙了。