|
Nejčastější chyby ve Smalltalku
(přeloženo/upraveno z originálu Classic Smalltalk Bugs by Ralph Johnson -- University of Illinois at Urbana-Champaign)
Úvod: Každý programovací systém/jazyk je náchylný k dělání určitých nejčastějších chyb. Dobrý programátor se musí naučit se těmto chybám vyhýbat a Smalltalk není výjimkou.
Přestože Smalltalk mnoho častých chyb eliminuje (např. lineární průchod polem nebo seznamem - stačí použít zprávu do:), tak zde nabízím seznam těch nejčastějších/začátečnických chyb.
Důvody: Existuje hned několik pádných důvodů, proč se zabývat shromažďováním nejčastějších chyb.
- pomoc zkušenějším programátorům při testování, ladění a správném návrhu programů.
- snížení frekvence výskytu chyb u začínajících programátorů, kteří jich budou alespoň částečně ušetřeni.
- Znalost tohoto typu chyb může designerům Squeaku pomoci jej upravit, aby se snažil je eliminovat nebo psát různé automatické kontroly
Poznámka překladatele:
Původní dokument by upraven a doplněny přímo pro konkrétní implementaci Smalltalku jménem Squeak.
PS: Jména tříd a metod budou zvýrazněny kurzívou. Je-li potřeba u zprávy uvést i třídu, která jí rozumí, tak se píše: Trida>>zprava.
Bug 1: Třídy proměnné velikosti
(Variable-sized classes)
Set, Dictionary a OrderedCollection jsou třídy proměnné velikosti (mohou růst).
Rostou tak, že vytvoří kopii sama sebe a přenesou se do této kopie (zpráva become: ).
Pokud od některé této třídy dědíte do své vlastní nové podtřídy a přidáváte nějakou instanční proměnnou, tak si musíte dát velký pozor, aby se prováděla kopie i přidaných proměnných (např. právě při růstu), jinak může docházet k záhadné ztrátě dat (v nepředpovidaném místě výpočtu).
Smalltalk-80 R4.0 (a asi i některé starší verze) měli metodu
copyEmpty: v abstraktní třídě Collection, kterou bylo nutno předefinovat pokud jste odvozovali vlastní podtřídu (kde přibyly nějaké nové instanční proměnné), takže
řešení je prosté: stačí napsat ve své třídě vlastní verzi metody copyEmpty:.
Je dobré podotknout, že není problém vytvořit si pomůcku, která by kontrolovala při vytváření vlastní třídy, zda dědí z Collection (nebo některé její podtřídy, isKindOf:) a zda přidává nějaké nové instanční proměnné a v takovém případě automaticky vytvořit metodu copyEmpty: (dokonce i včetně jejího kódu).
Práce s kolekcemi
(Collection bugs)
Bug 2: zpráva add: vrací svůj argument
U každé rostoucí kolekce, zpráva add: vrací svůj argument a ne příjemce této zprávy, jak někteří mylně předpokládají.
Tito pak chybně píšou (c add: x) add: y (výraz je syntakticky správně, ale sémanticky pouze pokud byste chtěli prvek y přidat do prvku x místo do kolekce c), když chtěli ve skutečnosti napsat c add: x; add: y nebo c add: x. c add: y .
Poznamenejme, že toto je dobrý případ použití zprávy yourself, takže můžete napsat např.:
(Set new
add: x;
add: y;
...;
yourself).
Výsledkem tohoto výrazu je nová instance třídy Set s vloženými prvky x, y, ...
Existuje dobrý důvod proč add: vrací svůj argument (a pokud byste se rozhodli jej implementovat tak, že by vracel příjemce, tak minimálně zmatete každého jiného smalltalk-programátora na světě).
Metoda add: vracející svůj argument vám často ušetří nutnost využít dočasnou proměnnou. Vkládaný objekt můžete vytvářet přímo ve výrazu pro argument a případné dodatečné zprávy poslat až výsledku této zprávy add:.
(c add: String new) zprávaProString: 'arg-něco'.
A pokud chcete přistupovat ke kolekci, tak stačí použít zprávu yourself a nebo využít mechanismus kaskády zpráv (pomocí středníku) tak, jak bylo předvedeno výše.
Bug 3: změna kolekce během interace nad ní samotnou
(changing collection while iterating over it)
Nikdy, nikdy a do třetice nikdy byste neměli iterovat (procházet v cyklu) přes kolekci, jež je iterační smyčkou nějak modifikována (pořádí či počet jejích prvků). Budou-li elementy kolekce odstraňovány během iterace, pak může dojít k vynechání nebo může být některý element vybrán/zpracován dvakrát. Místo toho udělejte nejprve kopii kolekce, kterou chcete procházet, pomocí metody copy, např.:
aCollection copy do: [:each | aCollection remove: each].
je správně, ale vypůstíme-li copy tak nikoliv.
Mario Wolczko navrhl řešení odstraňující tento problém (ovšem na úkor výkonnosti). Je nutno změnit třídy pro kolekce. Každa metoda sloužící pro iteraci vloží kolekci do speciální množiny právě-iterovaných kolekcí (IteratedCollections), pak se provede vlastní blok iterace a nakonec se kolekce zase z této množiny odstraní.
Kolekce jsou obvykle modifikovány užitím at:put: nebo basicAt:put:, takže se do nich musí přidat kontrola, zda se kolekce nevyskytuje v právě-iterovaných-kolekcích. Pokud je v takovém případě proveden pokus o modifikaci kolekce, tak je signalizována chyba. Buď můžete tuto techniku (kontrolu) používat vždy a nebo jen v ladící fázi vývoje vaší aplikace. Tyto změny tříd kolekcí (changes) jsou uloženy v souboru Iterator-check.st, který je k dispozici na ftp://ftp.sunet.se/pubd/lang/smalltalk/MANCHESTER/manchester/4.0/Iterator-check.st.
Bug 4: změna kopií kolekcí
(modifying copies of collections)
(TODO: zlepšit překlad a srozumitelnost, přesně jsem totiž tento bug nepochopil)
Pro objekt bývá běžné mít metodu pro přístup ke kolekci objektů, které lze modifikovat.
Ovšem někdy objekt vrátí kopii této kolekce, aby zajistil, že originální kolekce nebude modifikována.
Vy však namísto tohoto většinou předpokládáte, že tato zpráva změní původní kolekci.
V tomto případě bývá hlavním problémem špatná dokumentace, a člověk, který by rád modifikoval původní kolekci se tak dostává do potíží.
Například ScheduledControllers>>scheduledControllers.
Řešení jsou možná dvě:
- Mít lepší dokumentaci a zajistit, aby nikdo nemohl modifikovat kopie kolekci vracené z takovýchto objektů.
- objekty, které si nepřejí aby jimi vracené kolekce mohly být modifikovány vrací takzvané neproměnlivé (immutable) verze těchto kolekcí, které se sami postarají o zahlášení chyby, pokud byl proveden pokus o jejich modifikaci.
Bug 5: Chybějící ^
Je velmi jednoduché ve výraze metody vynechat návratovou šipku.
Pokud v metodě žádná návratová šipka není uvedena, tak Smalltalk automaticky vrací příjemce zprávy (tedy jako bychom tam napsali ^self).
Takže k tomu, abychom spustili celý řetěz mystifikací, stačí pouze jedno opomenutí návratové šipky, které může způsobit logickou chybu, kdy si myslíme, že naše metoda bude vracet něco jiného než ve skutečnosti vracet bude.
Pěkný příklad naleznete v bugu číslo 6.
Bug 6: metody vytvářející instanci třídy
(Class instance creation methods)
Napsat správně metodu pro vytvoření nové instance je zjevně netriviální a vyžaduje dobré pochopení rozdílů mezi objektem reprezentující třídu a instancí třídy a také znalostí mechanismu posílání a zpracování zpráv.
Správně je například:
new
^super new initialize
kde je třeba v každé třídě předefinovat metodu initialize sloužící pro kód inicializující počáteční hodnoty instančních proměnných.
Zde je příklad instanční metody initialize:
init
super init "pro inicializaci zděděných instančních proměnných"
"inicializace proměnných nově definovaných v této třídě"
Nyní si ukažme několik příkladů špatně napsaného konstruktoru:
Asi nejčastější chybou je zapomenutí návratové šipky (^)
super new init
To způsobí, že metoda místo vracení instance vrací třídu. Toto je speciální verze bugu číslo 5.
Další chybou je vytvoření nekonečné smyčky například takovýmto zápisem:
^self new init
,kdy se nekonečně zanořuje volání zprávy new.
Pokud v takovémto případě (či jakémkoliv jiném) Smalltalk neodpovídá (no response), tak můžete stisknout Alt-tečka (nebo Ctrl-tečka) a otevře se vám přerušení procesu (User Interrupt) a stisknutím tlačítka Debug otevřete okno debuggeru.
Pokud debugger ukazuje na zásobníků několik zpráv new, pak jste pravděpodobně udělali tento typ chyby.
Nakonec byste stejně měli definovat pouze jedno new pro každou hierarchii tříd a nechat potomky dědit tuto metodu a pro inicializaci využít předefinovávání metody initialize. Pokud budete new předefinovávat v každé třídě znova a znova, tak bude objekt při vytváření mnohokrát reinicializován, což určitě plýtvá časem a možná i pamětí!
Jeden způsob řešení je vytvořit v třídě Object metodu new, která při inicializaci zavolá např. metodu init a metoda init v třídě Object by nedělala nic.
Co ale v případě, pokud bychom potřebovali aby metoda init brala v úvahu i nějaké parametry? Právě z tohoto důvodu se toto řešení nepoužívá jako implicitní (tedy není obsaženo v třídě Object), ale je na každém programátorovi, aby o tomto způsobu věděl a patřičně jej přizpůsobil na svou hierarchii tříd, tak aby byl co nejefektivnější a návrhově čistý.
Bug 7: přiřazení třídě
(Assigning to classes)
OrderedCollection := 2
je naprosto legální výraz Smalltalku, ale má značně destruktivní účinky na vaši image!
Toto může být omezeno, pokud bychom upravili kompilátor, aby v takovémto případě vracel varování o pokus přiřazení hodnoty globální proměnné obsahující třídu.
Bug 8: zpráva become:
become: je velmi mocná operace, ovšem může být také velmi nebezpečná a není vůbec těžké si s ní zničit vlastní image.
Její hlavní použití je u rostoucích kolekcí (viz bug 1), protože umí každou referenci na starou verzi kolekce přepsat na reference na novou (větší) verzi té samé kolekce.
Její syntaxe se mírně liší pro různé verze Smalltalku: Smalltalk/V a Smalltalk-80
is a very powerful operation. It is easy to destroy your image
with it. Its main use is in growing collections (see bug #1), since
it can make every reference to the old version of a collection become a
reference to the new, larger version. It has slightly different semantics
in Smalltalk/V and Smalltalk-80, since "x becomes: y" causes every
reference to x and y to be interchanged in Smalltalk-80, but does not
change any of the references to y in Smalltalk/V.
Suppose that you want to eliminate all references to an object x.
Saying "x becomes: nil" works fine in Smalltalk/V, but will cause
every reference to nil to become a reference to x in Smalltalk-80.
This is a sure calamity. You want x to become a new object with no
references, such as in "x becomes: String new".
Bug 9: Recompiling bugs in Smalltalk/V
It is easy to have references to obsolete objects in Smalltalk/V
if you change code without cleaning things up carefully. For example,
the associations whose keys are the referenced names in the Pool
Dictionary are stored in the CompiledMethods at compile time. If you
create a new version of the Pool Dictionary and install it by simple
assignment then the compiled methods still refer to the old associations.
If you substitute a new instance of Dictionary or replace, rather than
update an association in a pool dictionary, you have to recompile all
methods using variables scoped to that Pool.
This is is also annoying when using ENVY, where the methods are under
strict control. Perhaps Pool Dictionaries should be be first-class
versioned pre-requisites of Classes, just like the class definition.
BTW we are using/VPM 1.4 with ENVY 1.3
1. If you prune & graft a subtree of your class structure you have to
make sure that all referencing methods are recompiled. Otherwise you
will run (or your customer, because this is only detected at run time)
into an Deleted class error message.
Thomas Muhr posted a "Bite" a while ago to handle this problem for
Smalltalk/V 286.
Bug 10: Otvírání oken
(Opening windows)
(Pozn.překladatele: Tento problém již není ve Squeaku aktuální)
Smalltalk-V a starší verze Smalltalk-80 neprovádí návrat odesilatele (sender) po otevření okna (řízení programu není vráceno do bloku, kde bylo otevření okna vyvoláno). Takže kód po otevření okna nebude nikdy vykonán.
Tato odchylka způsobuje mnoho frustrujících chyb. Například pokud se pokusíte otevřít 2 okna zároveň:
"pro Smalltalk-V:" TextPane new open. TextPane new open.
"nebo pro Smalltalk-80:" aScheduledWindow1 open. aScheduledWindow2 open.
, tak obdržíme pouze 1 otevřené okno a část kódu bude zapomenuta.
Tento problém byl opraven v Objectworks/Smalltalk R 4.1, takže tam předchozí kód vytvořil opravdu 2 okna, jak bychom očekávali.
Oprava pro dřívější verze Smalltalk-80 je použití metody openNoTerminate pro otevírání oken, která oknu nepředává řízení hned po jejím vytvoření.
Šikovným trikem je uložit si nové okno v globální proměnné, kterou můžete otestovat.
Aad Nales říká, že oprava pro Smalltalk/V je použít rozvětvení procesů (fork) při vytváření nového okna:
[Textpane open] fork.
Pokud programátor není spokojen s tímto způsobem, tak mu nezbývá nic jiného než změnit kód pro "dispatcher" a odstranit zprávu dropSenderChain, která je hlavní přičinou těchto problémů.
Bug 11: Bloky
(blocks)
(Pozn.překladatele: Tento problém již není ve Squeaku aktuální a týká se spíše starších implementací Smalltalku)
Blocks are very powerful, and it isn't hard for programmers to get
into trouble trying to be too tricky. To compound problems, the
two versions of Smalltalk have slightly different semantics for
blocks, and one of them often leads to problems.
Originally blocks did not have truly local variables. The block
parameters were really local variables in the enclosing method.
Thus,
x := 0.
(1 to: 100) do: [:z | x := x + z]
actually had three temporaries, x, y, and z. This leads to bugs
like the following
someMethod
| a b |
a := #(4 3 2 1).
b := SortedCollection sortBlock: [:a :b | a someOperation: b].
b addAll: a.
Transcript show: a.
When elements are added to b, the sortBlock is used to tell where
to put them, but this will change a and b. addAll: is OK, but
the "a" that gets displayed on the transcript will be an integer,
not an array.
Early versions of Smalltalk-80 (2.4 and before?) implemented blocks
like this, and Smalltalk/V still does. However, in current PPS
implementations, blocks are close to being closures. You can declare
variables local to a block, and the names of the block parameters are
local to the block. Most people agree that this is a much better
definition of blocks than the original one. Nevertheless, people
planning to use Smalltalk/V should realise that it has a different
semantics for blocks.
This difference can lead to some amusing problems. For example, here
is some code written by someone who had obviously learned Scheme.
aBlockArray := Array new: 4.
anotherArray := #(1 2 4 8).
1 to: 4 do: [ :anIndex |
aBlockArray at: anIndex put: [ (anotherArray at: anIndex) 2 ]].
The programmer expected that each block would be stored in the array
along with its own value of anIndex. If anIndex were just a local
variable of the method then this will not work. It assumes that
each execution of the block gets its own version of anIndex, and
Smalltalk/V and old Smalltalk-80 actually make each execution share
the same version.
So, if you are using Smalltalk/V then be careful not to reuse the
names of arguments of blocks unless you know that the blocks are
not going to have their lives overlap. Thus,
aCollect do: [:i | ...].
bCollect do: [:i | ...].
is probably OK because do: does not store its argument, so the
blocks will be garbage by the time the method is finished.
However, if the first block were stored in a variable somewhere
and evaluated during the execution of the second block then
problems would probably occur.
Bug 12: Cached Menus
Menus are often defined in a class method, where they are created
and stored in a class variable or a class instance variable. The
method will look something like this
initializeMenu
...
Note that accepting the method does change the menu. You
have to execute the method to change the class variable or class
instance variable. Often the initializeMenu method is invoked by
the class method initialize. This can lead to the strange effect
that you can initialize the menu by deleting the class and filing
it in again, but otherwise you don't seem to be able to change
the menu (because you haven't figured out that you should really
be executing the initializeMenu method).
To make matters worse, it is possible that each instance of the
controller, or model, or whatever has the menu, stores its own
copy of the menu in an instance variable. If that is the case then
it is not enough to execute initializeMenu, you also must cause each
object to reinitialize its own copy of the menu. It is often easier
to just delete the objects and recreate them.
Often a class will have a #flushMenus method to clear out all menus.
Typically the method that fetches the menu will check to see if it
is nil and invoke initializeMenu if it is. So, flushMenus will just
nil out the variable holding the menu. The best way to figure out
what is happening is to look at all uses of the variable. Smalltalk
experts rarely have problems with this bug, but it often confuses
novices.
Caching is a very common technique in Smalltalk for making programs
more efficient in both time and space. Caching of menus is one of
the simplest uses of caches, and other uses can create more subtle bugs.
Bug 13: Smalltalk/V class library
Thomas Muhr makes these comments about bugs in the Smalltalk/V
class library that you should know if you want to keep your
programs fast and correct.
2. Never use symbols to label objects if you are dealing with many
objects. This will slow down your system to an almost dead halt. Use
strings instead.
3. Never use Sets when you can otherwise assure the uniqueness. Look
at the implementation of "add:" for Sets and you'll know what I mean:
on every "add:" the new element is compared to all others resulting
into a nonlinear time for adding to Sets.
4. Do not think that if you "collect:" something from a
SortedCollection, that your result will be sorted as the origin,
unless you use the default sortBlock. This is one of the bugs provided
by the language vendor
Bug 14: Nesprávné použití kaskádové/kaskádních metod
(Using cascaded messages improperly)
(autor: jaeck@alc.com (William A. Jaeck)
I had created a subclass of OrderedCollection with an instance
method called with:. This is supposed to do the same thing as
add:.
Then, I implemented a class method called with:with: as
with: arg1 with: arg2
^ self with: arg1; with: arg2
This ended up producing a result as if I had implemented it as
^ self with: arg2
The correct implementation of with:with: is, of course
^ (self with: arg1) with: arg2
Bug 15: Používání třídních metod pro implementaci singulárního objektu (existuje vždy pouze 1 instance)
(Using class methods to implement a singleton object)
From: riks@ogicse.cse.ogi.edu (Rik Fischer SmOOdy)
A common "dirty trick" is to use a class with class methods as an
implementation for a "globally known" object which is expected
to be unique. This class is never expected to be instantiated:
instances would have no capabilities except those inherited by
default from #Object.
The trick usually works, but it qualifies as design by accident.
A tyro might study the object as an example and be misled.
(It is important to encourage re-use to keep the visible structure
as comprehensible as we can)
Future engineers might decide that there needs to be another "instance"
of this globally known object. I have seen an example (identity
of perpetrators withheld to protect the unconvicted) where the
second instance was implmented as a sub-class, with numerous methods
over-written to access a different class variable.
On Smalltalk V/MAC, a quick scan found (Color MTrap Compiler and LCompiler )
Smalltalk80 V4 comes with DefineOpcodePool but that may have other
redeeming social value.
The most egregious examples have come with other installed packages.
Mr. Manners instead approves of the way Smalltalk80 uses the globally
published objects named Transcript, Undeclared, and even Smalltalk itself.
Bug 16: Předefinování operátoru = ale ne operátoru hash
(Redefining = but NOT hash)
From: Mario.Winter@FernUni-Hagen.de (Mario Winter)
Kent Beck makes these comments about redefining = most often demands
redefining hash, because all subclasses of Set only use hash to compare
their Elements. So don`t wonder if your set contains elements which you
consider to be equal, if you have not redefined hash to compute the same
hash-value for ,equal" elements. Always remember that Object defines = as
=, that is equality is defined as identity.
Poděkování: Velké díky patří mnoha lidem, kteří se na tomto dokumentu podíleli:
- muhr@opal.cs.tu-berlin.de (Thomas Muhr)
- steinman@is.morgan.com (Jan Steinman)
- knight@mrco.carleton.ca (Alan Knight)
- mario@cs.man.ac.uk (Mario Wolczko)
- peterg@netcom.com (Peter Goodall)
- nales@cs.few.eur.nl (Aad Nales)
- scrl@otter.hpl.hp.com (Simon Lewis)
- msmith@volcano.ma30.bull.com (Mike Smith)
- dai@mrco.carleton.ca (Naci Dai)
- dcr0@speedy.enet.dec.com (Dave Robbins)
- randy@tigercat.den.mmc.com (Randy Stafford)
- Hubert.Baumeister@mpi-sb.mpg.de (Hubert Baumeister)
- eliot@dcs.qmw.ac.uk (Eliot Miranda)
- dmm@aristotle.ils.nwu.edu (donald)
- amir@is.morgan.com (Amir Bakhtiar)
- Piersol@Apple.com (Kurt Piersol)
- sullivan@ticipa.ti.com (Michael Sullivan)
- terry@zoe.network23.com (Terry)
- brent@uwovax.uwo.ca (Brent Sterner)
- frerk@informatik.uni-kl.de
- nicted@toz.buffalo.ny.us (Nicole Tedesco)
- riks@ogicse.ogi.edu (Rik Fischer Smoody)
- marten@feki.toppoint.de (Marten Feldtmann)
- mst@vexpert.dbai.tuwien.ac.at (Markus Stumptner)
- Mario.Winter@FernUni-Hagen.de (Mario Winter)
- jaeck@alc.com (William A. Jaeck)
Originální zdroj: ftp://st.cs.uiuc.edu/pub/Smalltalk/st-docs/classic-bugs.txt
Odkaz na tuto stránku
- Články, poslední úprava dne 17 Únor 2010 v 11:18:52 uživatelem localhost
|