Scope đối với người mới bắt đầu với Corona SDK

Là một người mới lập trình với Lua/Corona, có phải bạn đang rất bối rối bởi lỗi như thế này?
Runtime error /Users/yourname/Projects/AwesomeGame\menu.lua:4: attempt to index global 'button' (a nil value)
stack traceback:
        [C]: in function 'error'
        ?: in function 'gotoScene'
        /Users/yourname/Projects/AwesomeGame\main.lua:16: in main chunk
Nếu vậy, thì bạn cần phải tìm hiểu một khái niệm lập trình rất quan trọng được gọi là phạm vi (scope). Scope tác động vào khả năng của Lua để "nhìn thấy" và quản lý mối quan hệ giữa các biến (như trong lỗi trên), các hàm đã tồn tại với các biến và các hàm khác.

Trong hướng dẫn trước, chúng ta đã học được tầm quan trọng của việc định dạng đúng các code và mô tả đúng tên các biến. Một chủ đề khác cũng được thảo luận là làm thế nào để giữ được khả năng quản lý các hàm. Khi một hàm trở nên quá dài, nó sẽ trở nên khó đọc, và một trong những lý do tại sao một hàm lại dài là vì nó có các hàm khác lồng vào bên trong nó. Mặc dù có những lý do khác để có thể đưa các hàm vào những hàm khác, nhưng nói chung nó xảy ra bởi vì khái niệm về phạm vi - scope.

Định nghĩa về Scope

Khi bạn nghĩ về những thuật ngữ như "kính viễn vọng" hoặc "kính hiển vi", bạn sẽ nghĩ ngay về các thiết bị giúp bạn có thể nhìn và thấy mọi thứ trong một giới hạn - ví dụ như những thứ rất nhỏ trong một kính hiển vi. Trong thuật ngữ lập trình, phạm vi được sử dụng để xác định các phần nào của code có thể "nhìn thấy" các biến, các đối tượng, và các hàm mà bạn đã tạo. Trong Lua, có hai phạm vi kiểm soát chủ yếu là: global và local.

Global
Biến/Hàm global có thể được nhìn thấy ở bất cứ nơi nào trong chương trình của bạn. "Good, tôi sẽ sử dụng phạm vi global cho tất cả mọi thứ!", Bạn có thể nói như thế, nhưng các đối tượng global khá rắc rối và bạn cần phải hiểu được những hạn chế và điểm yếu của chúng. Trong thực tế, các lập trình viên mới nên tránh sử dụng chúng. Một lý do là vì chúng không bị giới hạn về “tầm nhìn” – dẫn đến chúng làm giảm hiệu năng của ứng dụng. Chúng cũng có những rủi ro, nếu bạn chỉ định một global trong một module và bạn vô tình tạo ra một global khác cùng tên trong module khác, nó sẽ “xung đột” với cái trước đó.

Local

Trong Lua, phạm vi được yêu thích là local, bạn có thể kiểm soát bằng cách khai báo local trước khi bạn định nghĩa một biến/hàm. Ví dụ như:
local someVariable
local function someFunction()
    -- Làm gì đó...
end
Nếu bạn đặt trước một biến hoặc một hàm với local khi bạn lần đầu tiên tạo ra nó, nó sẽ trở nên khả dụng đối với khối code này và bất kỳ khối "con" nào chứa bên trong nó (nếu bạn không quen thuộc với khái niệm của các khối code, vui lòng đọc hướng dẫn trước). Hãy xem xét ví dụ này:
local function addTwoNumbers( number1, number2 )
    -- Tạo ra biến local "sum" cho hàm "addTwoNumbers()"
    local sum = number1 + number2
    print( sum )  -- Hoạt động!
end
print( sum )  -- KHÔNG hoạt đọng (prints ra "nil")

Trong trường hợp này, hàm addTwoNumbers() định nghĩa một khối code mới. Bên trong khối này, chúng ta tạo ra một biến mới có tên sum. Biến này có từ khóa local, có nghĩa là nó chỉ có thể nhìn thấy bởi hàm addTwoNumbers(). Tất cả các code trong module hay toàn bộ project đều không thể "nhìn thấy" các biến sum. Vì vậy, khi hàm kết thúc, sẽ không có một biến nào được gọi là sum và sẽ nil nếu bạn print.

Bây giờ xem xét khối này:
local function addTwoNumbers( number1, number2 )
    local sum = number1 + number2
    if sum < 10 then
        print( sum .. " is less than 10" )
        local secondSum = sum + 10
    end
    print( secondSum )  -- KHÔNG hoạt động!
end

Giống như ví dụ đầu tiên, sum là local của hàm addTwoNumbers(). Điều đó cho phép chúng ta có thể add thêm hai con số, và nó cũng có thể nhìn thấy bên trong khối bắt đầu từ if sum < 10 then. Tuy nhiên, bên trong khối if-then, một biến local (secondSum) được tạo ra, và biến đó trở thành local của khối if-then. Vì vậy, những nỗ lực để print secondSum ngoài khối if-then sẽ trả về kết quả nil. Hàm bên trong một hàm khác Bởi vì các lập trình viên mới phải xử lý vấn đề về phạm vi, các thư viện hướng sự kiện như Composer có thể tạo ra căng thẳng cho họ trong việc quản lý code. Đối với các nhà phát triển mới, những thách thức thật sự là họ cần phải hiểu các vấn đề cơ bản khi scene:create() được gọi, khi thực hiện xong các công việc và khi scene:show() được gọi. Một phương pháp mà một số người mới bắt đầu thường làm theo là đặt các hàm con bên trong hàm scene:create()- hoặc tệ hơn, di chuyển tất cả các đoạn code tạo scene vào hàm scene:show() vì họ không biết được phạm vi làm việc của hai hàm đó. Trong một scene điển hình, bạn cần touch handlers cho các đối tượng, event handlers cho các button, và/hoặc collision handlers để phát hiện khi các đối tượng chạm vào nhau. Nhưng các hàm này có thể gọi các hàm khác, và nếu bạn đặt tất cả chúng vào trong scene:create() hoặc scene:show(), hàm sẽ trở nên rất dài và khó quản lý. Một vấn đề phổ biến của phạm vi là khai báo các thứ như là local bên trong hàm scene:create() khi bạn cần phải truy cập vào chúng ở một nơi khác. Hãy xem xét ví dụ này:
-- Required composer
local composer = require( "composer" )
local scene = composer.newScene()
local widget = require( "widget" )  -- Require widget library cho button

-- Đây là nơi bạn tạo các biến và các hàm bạn cần để được "nhìn" bởi các hàm Composer dưới đây
local scoreText  -- Xem ghi chú #2 bên dưới
local score
local function addToScoreButtonEvent( event )
    if event.phase == "ended" then
        score = score + 1
        scoreText.text = tostring( score )
    end
    return true
end

-- Bây giờ xác định các hàm event scene
function scene:create( event )
    local sceneGroup = self.view
    -- 1. Khai báo "background" local ở đây vì  chúng ta không cần truy cập vào nó ở bất cứ nơi nào trong scene
    local background = display.newRect( 0, 0, display.contentWidth, display.contentHeight )
    background.x = display.contentCenterX
    background.y = display.contentCenterY
    background:setFillColor( 0 )
    -- Insert background vào scene group để Composer quản lý nó
    sceneGroup:insert( background )

    --2. Nếu chúng ta thực hiện local cho đối tượng này trong hàm "scene:create()", chúng ta sẽ không thể truy cập và cập nhật nó bất cứ nơi nào bên ngoài, đó là lý do tại sao chúng ta khai báo nó như là local ở gần đầu của module
    scoreText = display.newText( "", 125, 32, native.systemFontBold, 64 )
    scoreText:setFillColor( 1, 1, 1 )
    scoreText.x = display.contentCenterX
    scoreText.y = 50
    sceneGroup:insert( scoreText )
  
-- 3. Ok nếu chúng ta local button trong phạm vi này vì chúng ta không cần phải truy cập vào đối tượng ở nơi khác; Tuy nhiên, chúng ta cần một hàm để xử lý khi button được bấm ("addToScoreButtonEvent ()") mà có thể được đưa vào ở đây, nhưng nó sẽ làm cho hàm này dài hơn vì vậy, chúng ta khai báo nó ở trên.
    local addToScoreButton = widget.newButton({
        id = "Add to Score",
        label = "Add to Score",
        width = 200,
        height = 32,
        fontSize = 32,
        onEvent = addToScoreButtonEvent
    })
    addToScoreButton.x = display.contentCenterX
    addToScoreButton.y = display.contentHeight - 40
    sceneGroup:insert( addToScoreButton )
end
function scene:show( event )
    local sceneGroup = self.view
    if event.phase == "will" then
        -- Thực hiện các thứ ở đây để "reset" scene của bạn; cả hai đối tượng đã được xác định ở gần đầu trang, vì vậy bạn có thể truy cập chúng ở đây
        score = 0
        scoreText.text = tostring( score )
    elseif event.phase == "did" then
        -- thực hiện những thứ sẽ xảy ra khi scene được hiển thị đầy đủ trên màn hình
    end
end
function scene:hide( event )
    local sceneGroup = self.view
    if event.phase == "will" then
        -- thực hiện những thứ khi scene sắp ra khỏi màn hình
    end
end
function scene:destroy( event )
    local sceneGroup = self.view
end
-- Add scene event listeners and return "scene" object
scene:addEventListener( "create", scene )
scene:addEventListener( "show", scene )
scene:addEventListener( "hide", scene )
scene:addEventListener( "destroy", scene )
return scene

Hãy bắt đầu bằng việc phân tích scene này. Nhìn bề ngoài, nó sẽ có một background, một đối tượng text để hiển thị số điểm hiện tại, và một button để tăng điểm số.

Tiếp theo, xem xét với khái niệm Composer tiêu chuẩn: chúng ta tạo ra tất cả các thành phần của giao diện người dùng (UI) bên trong scene:create(). Điều đầu tiên chúng ta tạo ra là background. Đối với scene đơn giản này, các đối tượng background này sẽ không cần phải truy cập vào nữa sau khi nó được tạo ra, vì vậy chúng ta xác định nó là local trong hàm scene:create(). Tuy nhiên, các biến để theo dõi số điểm (score) và các đối tượng text để hiển thị số điểm (scoreText) sẽ cần phải được truy cập (nhìn thấy) ở những nơi khác - do đó, chúng ta xác định chúng ở gần đầu của module chứ không phải bên trong một hàm hoặc khối:
local score
local scoreText

Lưu ý rằng các biến vẫn chưa được khởi tạo với bất kỳ giá trị nào, chúng ta chỉ đơn giản là khai báo chúng, chúng vẫn chưa đươc tạo cho đến khi các đối tượng text được tạo ra trong scene:create() và gán biến scoreText cho chúng.

Đối với các widget button để gia tăng điểm số, chúng ta không cần phải truy cập vào nó sau khi nó được tạo ra, tuy nhiên nó đòi hỏi một hàm xử lý onEvent (addToScoreButtonEvent). Đây là một trường hợp tốt để tạo hàm thiết lặp trực tiếp trên widget.newButton(). Điều đó sẽ giữ cho nó gần hơn với các code của button, nhưng nó sẽ làm cho hàm scene:create() xa hơn. Vì vậy, để rõ ràng hơn trong khối, chúng ta đã viết hàm bên trên và bên ngoài hàm tạo. Đối với hàm này, nó thực hiện hai việc: add thêm 1 cho số điểm, sau đó nó cập nhật các đối tượng scoreText.

Cuối cùng, chúng ta hãy kiểm tra hàm scene:show(). Trong "will" phase của hàm này, chúng ta khởi tạo biến mà chúng ta muốn nó reset mỗi khi chúng ta quay lại scene (hàm scene:show() sẽ được gọi mỗi khi scene được hiễn thị trên màn hình). Trong trường hợp này, chúng ta khởi tạo điểm số là 0 và tạo ra những giá trị đầu tiên cho scoreText. Một lần nữa, nếu chúng ta đã thực hiện các biến local bên trong scene:create(), hàm scene:show() sẽ không có quyền truy cập vào chúng.

Sức mạnh của thụt đầu dòng

Trong hướng dẫn trước, chúng ta đã nói về tầm quan trọng của thụt đầu dòng là để code của bạn sẽ dể đọc. Thụt đầu dòng cùng với scope sẽ giúp bạn tốt hơn! Khi bạn thụt mỗi khối đúng cách và bạn sử dụng một từ khóa local, biến local đó sẽ không thể được nhìn thấy bất cứ nơi nào bên trái của khối đó. Một mẹo trực quan tốt để biết nơi nào biến của bạn có thể được nhìn thấy.

Biến Index trong vòng "for"

Khi bạn viết một khối code như:
local i = 999
for i = 1, 10 do
    -- Do something
    print( i )  -- prints 1, 2, 3, .. 10
end
print( i )  -- prints ra 999

Biến i là local của vòng for. Khi vòng lặp kết thúc, i sẽ quên đi giá trị cuối cùng của nó trong vòng lặp và trả về định nghĩa trước của nó (thường là nil). Trong ví dụ trên, một biến local i đã được thiết lập là 999 trước hàm for và trong khi vòng lặp thực hiện, giá trị của nó đại diện cho các giá trị từ 1-10. Khi vòng lặp được thực hiện xong, i là local của vòng lặp sẽ biến mất và giá trị 999 trước đó của nó sẽ là giá trị được nhìn thấy.

Kết luận

Hy vọng rằng, bài viết này đã minh họa minh họa được khái niệm scope một cách tương đối đơn giản hơn. Khi bạn sử dụng các từ khóa local, các biến chỉ có thể được truy cập ở mức khối đó hoặc trong bất kỳ khối con của nó mà thôi. Với ý nghĩ đó, nếu các biến hoặc các hàm bạn cần làm cho chúng được nhìn thấy ở mức độ rộng hơn, chỉ đơn giản là di chuyển chúng lên trên/bên ngoài khối hoặc xác định chúng ở gần đầu (top) của các module.

Không có nhận xét nào :

Đăng nhận xét