Question

What's the advantage and disadvantage that using string.format("str", ...) instead of "str":format(...) in Lua?

I found a interesting problem in Lua. Lua provides string.format function to format the string, and the first parameter of this function is the string you want to format.

Coincidentally, Lua allows : operator to make a object call a function with the first parameter is itself, which means the following statement can run normally:

local myWord = "Hello, %s"

-- two ways to format:

string.format(myWord, "World")

myWord:format("World")

Is there any difference between those two style? If not, which one is better for me to use in my program?

 2  28  2
1 Jan 1970

Solution

 4

As often in Lua, there technically is a subtle difference. But that difference most probably won't matter to you at all. I generally prefer the OOP-style for calling string methods, since it's more concise and doesn't require string to be in scope.

If myWord is guaranteed to be a string and the string metatable has not been tampered with, these are equivalent and the latter is just syntactic sugar for the former.


As for the nitty-gritty details:

string.format(myWord, "World") looks string up in _ENV (typically _G, the global table), then accesses the format field of that, then calls that with myWord and "World" as arguments.

myWord:format("World") indexes myWord, then it accesses the format field and calls that with arguments myWord, "World".

If myWord is a string, indexing myWord will hit the metatable, which has the string table set as __index, unless someone did getmetatable"".__index = {} or similar tampering with the string metatable.

(An oversight that happens sometimes in Lua sandboxes is to forget to make the string metatable inaccessible.)

Perhaps the most relevant subtle difference in behavior is that string.func(s, ...) will coerce s to a string if it is a number, whereas s:func(...) will throw an "attempt to index a number value".

This may be relevant if you're maintaining a legacy Lua API and "refactoring" it to use the latter style, accidentally breaking the code of API users who relied on the implicit number to string coercion.

Another maybe-relevant difference is that s:func(...) would let you supply a table s with a metatable set such that s:func(...) does something sensible: It plays better with polymorphism. For example you could have a GraphemeString "class" which supports (its own version of) s:sub, but operates on graphemes rather than bytes. If you're using the "OOP" style, you could easily swap one for the other. (If you're using the "imperative" style, you could still monkey-patch string.format etc., but that would be messy.)

2024-07-23
Luatic