WoW:UI best practices: Difference between revisions
(Advise against careless use of throw-away tables and link HOWTO discussing minimizing their impact.) |
m (→Use local variables: wiki-typo) |
||
| Line 11: | Line 11: | ||
One stumbling block for Lua programmers coming from other languages is the fact that all variables are global unless specified otherwise. Many times, the use of global variables is necessary: saved variables are created via the global environment; all API functions and UI frames are in the global namespace. However, globals come at a cost of both performance, and naming conflicts. | One stumbling block for Lua programmers coming from other languages is the fact that all variables are global unless specified otherwise. Many times, the use of global variables is necessary: saved variables are created via the global environment; all API functions and UI frames are in the global namespace. However, globals come at a cost of both performance, and naming conflicts. | ||
Globals in Lua are actually fields in a table called _G. In order to access a global variable, Lua internally translates the variable name to _G["variable name"]. Local variables, on the other hand, are stored on a stack. Stack access is always faster than indexing a table. In code where performance is important (for instance an [[UIHANDLER_OnUpdate|OnUpdate handler), it's often beneficial to create a local alias to a global function: | Globals in Lua are actually fields in a table called _G. In order to access a global variable, Lua internally translates the variable name to _G["variable name"]. Local variables, on the other hand, are stored on a stack. Stack access is always faster than indexing a table. In code where performance is important (for instance an [[UIHANDLER_OnUpdate|OnUpdate handler]]), it's often beneficial to create a local alias to a global function: | ||
local Foo = Foo | local Foo = Foo | ||
Revision as of 22:22, 24 March 2007
With the low barrier to entry, writing mods has become very popular. While this has greatly benefited the WoW community, it does have a dark side. There are idiosyncrasies in both the Lua language and the World of Warcraft API that many authors overlook. This can lead to poorly written programs as measured by performance, memory usage, interference with other addons and the default UI, etc. The techniques gathering on this page will help help addon authors, experienced and otherwise, make the most out of WoW's environment safely and efficiently.
General Scripting
These tips relate to Lua scripting in general. They are offered to help you write safer and more efficient code, and are applicable to non-WoW scripts as well.
Use local variables
One stumbling block for Lua programmers coming from other languages is the fact that all variables are global unless specified otherwise. Many times, the use of global variables is necessary: saved variables are created via the global environment; all API functions and UI frames are in the global namespace. However, globals come at a cost of both performance, and naming conflicts.
Globals in Lua are actually fields in a table called _G. In order to access a global variable, Lua internally translates the variable name to _G["variable name"]. Local variables, on the other hand, are stored on a stack. Stack access is always faster than indexing a table. In code where performance is important (for instance an OnUpdate handler), it's often beneficial to create a local alias to a global function:
local Foo = Foo
for i = 1, 10000 do
Foo()
end
Naming conflicts are sometimes a more pressing concern. The effects are usually much more noticeable. If two addons define print functions that work in slightly different ways, one of the addons isn't going to be too happy when it tries to use its own. Another problem can be taint... If you accidentally use the name of some variable that blizzard code uses (e.g. arg1), you can introduce taint.
These problems are easily solved by declaring functions and variables local. There are other approaches as well, but this is by far the simplest.
Hook functions safely
See also: HOWTO:_Hook_functions_in_a_safer_way
You should do your best to write your addon in such a way that you don't need to hook functions. Even when it is necessary, most of the time hooksecurefunc is sufficient. However, there are occasional cases where "traditional" hooking must be used. When you do, remember to pass all parameters and return all results. Example:
local OrigFunc = Func
Func = function(foo, bar, ...)
DoStuff()
local blah, baz, rofl = OrigFunc(foo, bar, ...)
DoOtherStuff()
return blah, baz, rofl
end
Even if the function only takes two parameters, you should always use the vararg (...). This will help future-proof your hook. If parameters are added to the function, your addon will safely ignore them while still passing them along.
There is actually a slight problem with the above example. If the number of return values from the function increases, your addon will likely cause erros. If you can call the original function as your last step, this is easy to deal with:
return OrigFunc(foo, bar, ...)
If you need to do processing based on the return value, you'll need to create a table to store the results of the call:
local ret = {OrigFunc(foo, bar, ...)}
DoStuff()
return unpack(ret)
Make efficient use of conditionals
If you have a series of if conditionals, test the most efficient ones first. For example:
if Efficient() then
DoStuff()
elseif LessEfficient() then
DoOtherStuff()
elseif HorriblySlow() then
DoSomething()
else
Cry()
end
There are exceptions to this rule, though... If one of the less efficient conditions is more likely to be the case, you should test it first. This way, instead of running both the fast test and the slow test most of the time, it only runs the slow test. E.g.:
if SlowButCommon() then
DoStuff()
elseif FastButRare() then
DoOtherStuff()
end
Shortcut evaluation
Lua uses shortcut evaluation of conditionals. This means it only evaluates enough of the condition to know for certain whether it's true or false. In the case of or, the whole condition is known to be true as soon as one operand is true. For and, the whole condition is known to be false as soon as one operand is false. You can take advantage of this to add a bit of efficiency:
if Fast() or Slow() then
DoStuff()
elseif LikelyToBeFalse() and LikelyToBeTrue() then
DoStuff()
elseif LikelyToBeTrue() or LikelyToBeFalse() then
DoStuff()
end
Minimize use of throw-away tables
Tables in Lua, being powerful instrument, can cause your addon to pollute memory with unnecessary garbage if you're not careful, as tables are one of value types that are not reused automatically. It is better not to use repeatedly generated throw-away tables unless they provide far easier to read solution, faster code or if your task is impossible to handle without them at all. In some cases you can minimize garbage generation when using those as explained in HOWTO: Use Tables Without Generating Extra Garbage.
API & XML
This section applies specifically to the World of Warcraft UI environment.
Use local event handler parameters
With the introduction of WoW 2.0, widget event handlers now provide local parameters. As mentioned in a previous section, accessing local values is more efficient than accessing globals. Here are a few examples of the old way and how they should be implemented now:
Code directly in XML
Old
<OnEvent>
if event == "SOME_EVENT_NAME" then
this:Show()
elseif event == "SOME_OTHER_EVENT" then
DEFAULT_CHAT_FRAME:AddMessage(format("%s: %s", arg1, arg2))
end
</OnEvent>
New
<OnEvent>
if event == "SOME_EVENT_NAME" then
self:Show()
elseif event == "SOME_OTHER_EVENT" then
DEFAULT_CHAT_FRAME:AddMessage(format("%s: %s", select(1, ...), select(2, ...)))
end
</OnEvent>
XML calling Lua function
Old XML
<OnEvent>
MyEventHandler()
</OnEvent>
Old Lua
function MyEventHandler()
if event == "SOME_EVENT_NAME" then
this:Show()
elseif event == "SOME_OTHER_EVENT" then
DEFAULT_CHAT_FRAME:AddMessage(format("%s: %s", arg1, arg2))
end
end
New XML
<OnEvent>
MyEventHandler(self, event, ...)
</OnEvent>
New Lua
function MyEventHandler(frame, event, firstArg, secondArg)
if event == "SOME_EVENT_NAME" then
frame:Show()
elseif event == "SOME_OTHER_EVENT" then
DEFAULT_CHAT_FRAME:AddMessage(format("%s: %s", firstArg, secondArg))
end
end
Lua only
Old
frame:SetScript("OnEvent", function()
if event == "SOME_EVENT_NAME" then
this:Show()
elseif event == "SOME_OTHER_EVENT" then
DEFAULT_CHAT_FRAME:AddMessage(format("%s: %s", arg1, arg2))
end
end)
New
frame:SetScript("OnEvent", function(frame, event, firstArg, secondArg)
if event == "SOME_EVENT_NAME" then
frame:Show()
elseif event == "SOME_OTHER_EVENT" then
DEFAULT_CHAT_FRAME:AddMessage(format("%s: %s", firstArg, secondArg))
end
end)