以下大部分是正確的;我可能錯過了什麼或誤解了什麼;閱讀鏈接的文檔(它太短了)並在評論中解僱 - 謝謝。
不好比喻: 一個類是一個千篇一律的東西。 一個實例是由曲奇切割器製成的曲奇。
類比的問題: 我也打算打電話給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 將矩陣參數複製到參數值中,並可能返回另一個副本,這樣就不那麼笨拙了。