Function và procedure
Hàm và thủ tục
Hàm và thủ tục
Khi viết chương trình, chúng ta không nên viết tất cả mã chung một chỗ mà cần tách chúng ra thành những phần nhỏ hơn, gọi là các chương trình con. Điều này có một số ưu điểm sau:
Chia chương trình thành những phần khác nhau, dễ xem xét và bảo trì
Có thể tái sử dụng các đoạn mã ở các vị trí khác nhau
Hàm và thủ tục gọi chung là chương trình con (subroutines). Mình có thói quen dùng từ hàm cho cả hàm và thủ tục, nên từ giờ về sau, khi mình nói hàm thì các bạn hiểu chung là chương trình con nhé.
Hàm (function) và thủ tục (procedure) đều gồm 3 phần chính:
Phần đầu: Định nghĩa hàm và các tham số
Phần khai báo: Các khai báo var, type, const, ... cho hàm sử dụng
Phần thân hàm: Chứa các lệnh
Các bạn đã làm quen với khá nhiều hàm trong delphi. Ví dụ như hàm tính sin(), hàm Length() để tính độ dài chuỗi, ...
Những hàm như vậy nhận vào một số biến hoặc hằng (input), rồi tính toán và trả về một giá trị (output). Như vậy gọi là hàm.
Như trên đã nói, hàm gồm 3 phần. Chúng ta hãy xem qua ví dụ về một hàm đơn giản
function CongThem1(So: Integer): Integer;
begin
Result := So + 1;
end;
Hàm trên nhận vào một số Integer, tăng số đó lên 1 và trả về hàm. Chúng ta có thể sử dụng hàm trên như sau.
var
A: Integer;
begin
A := 5;
writeln(CongThem1(A)); // In ra 6
end;
Như vậy, hàm có thể sử dụng như một biến, có thể thêm vào hàm khác, hoặc dùng để tính toán.
Quay lại đoạn định nghĩa hàm (đoạn code function), chúng ta có một số khái niệm sau:
Từ khóa function: Khai báo hàm
Tên hàm: CongThem1
Danh sách tham số trong cặp ngoặc tròn: So: Integer. Danh sách tham số có thể có nhiều số khác nhau, cách nhau bởi dấu chấm phẩy
Kiểu trả về: Integer
Hàm này không có phần khai báo var, const hay gì cả (nếu có thì phải đặt trước begin end của hàm)
Thân hàm: trong cụm begin end.
Hàm được đại diện bởi biến Result. Bạn gán giá trị cho Result, khi thực hiện hàm xong thì giá trị Result chính là giá trị mà hàm trả về. Kiểu dữ liệu của Result chính là kiểu dữ liệu của hàm.
Thủ tục về cơ bản là một hàm, nhưng không có kiểu trả về, nên không có biến Result. Một số ngôn ngữ khác không có khái niệm thủ tục, nhưng delphi phân biệt rạch ròi giữa 2 khái niệm này.
Thủ tục không dùng để tính toán. Thủ tục chỉ thực hiện lệnh, nên không thể có trong biểu thức tính toán. Ví dụ:
begin
Hello; // Thủ tục Hello
GoByCar(10); // Chạy xe hơi 10 km
writeln(Hello); // Sai
Hello := 1 + Hello; // Sai
end;
Khai báo thủ tục tương tự như hàm, nhưng thủ tục không có giá trị trả về và khai báo bằng từ khóa procedure.
procedure Hello;
begin
writeln('Hello');
writeln('My name is delphi');
end;
procedure GoByCar(SoKm: Byte);
begin
writeln('Go ...');
end;
Như trên là 2 thủ tục Hello và GoByCar. Hello thì không có danh sách tham số, còn GoByCar có một tham số SoKm: Byte.
Có khá nhiều bạn không phân biệt được 2 khái niệm này:
Tham số (parameter): là những biến, hằng trong khai báo hàm
Đối số (argument): là những biến, hằng, giá trị được truyền vào khi gọi hàm.
Ví dụ nhé
function Pytago(a, b: Byte): Single;
begin
Result := Sqrt(a * a + b * b);
end;
var
x, y: Byte;
begin
writeln(Pytago(3, 4)); // In ra 5.0000E+1 gì đó nhưng đại loại là 5
x := 6;
y := 8;
writeln(Pytago(x, y)); // In ra 1.0000E+1 số dạng khoa học nhưng giá trị là 10
end;
Chúng ta đã xây dựng hàm Pytago để tính độ dài cạnh huyền từ 2 cạnh góc vuông. Vậy, đâu là tham số, đâu là đối số:
Tham số: a, b
Đối số: 3, 4 (trong lời gọi hàm Pytago lần 1) và x, y (lần 2)
Lưu ý: Tham số và đối số có thể giống nhau, nhưng nó là 2 thứ khác nhau.
Khi có lời gọi hàm, thì đối số sẽ được chuyển đến hàm làm tham số. Đó gọi là parameters passing (truyền tham số).
Có 2 kiểu truyền tham số trong delphi:
Pass by value: Truyền bằng giá trị
Pass by reference: Truyền bằng tham chiếu
procedure PassByValue(A: Byte);
begin
A := 5;
end;
procedure PassByReference(var A: Byte);
begin
A := 5;
end;
var
A: Byte;
begin
A := 10;
PassByValue(A);
writeln(A); // A vẫn là 10
A := 10;
PassByReference(A);
writeln(A); // A đổi thành 5
end;
Hai hàm Pass trên đều giống nhau, nhưng chỉ khác là PassByValue thì có từ khóa var trước tham số. Nếu không có var, thì là truyền theo giá trị (pass by value), nếu có var (hoặc const, out) thì là truyền theo tham chiếu (pass by reference).
Điểm khác nhau là:
Pass by value: Giá trị của tham số giữ nguyên sau khi hàm thực hiện xong
Pass by reference: Giá trị tham số thay đổi do hàm thực hiện
Cơ chế hoạt động:
Pass by value: Một biến mới được tạo ra trên bộ nhớ stack làm tham số, sau đó giá trị của đối số được copy vào biến tham số. Hàm sẽ thao tác trên tham số mà không tác động đến đối số, do đó sau khi thực hiện hàm, đối số không thay đổi
Pass by reference: Một tham chiếu được tạo ra đến đối số. Các thao tác xử lý trên tham chiếu này cũng là thao tác với đối số. Thế nên sau khi hàm xong thì đối số có thể bị thay đổi.
Do vậy, pass by reference sẽ có tốc độ nhanh hơn do không phải copy giá trị. Nhưng đừng dại mà lúc nào cũng dùng pass by reference, có thể sẽ gây lỗi chương trình đó.
Ngoài 2 cách pass tham số như trên, thì còn có 2 tham số nữa là tham số const và tham số out. Về cơ bản, const và out đều là pass by reference nhưng có một số khác biệt.
Const sử dụng truyền bằng tham chiếu. Nhưng trong hàm nó không được phép thay đổi (chỉ đối với các kiểu dữ liệu thông thường, còn như object thì sẽ bị thay đổi). Như vậy, đoạn code sau là sai.
procedure ABC(const A: Byte; S: string);
begin
writeln(A, ' ', S);
A := 10; // Reng reng, báo lỗi
end;
Mặc dù vậy, khi truyền các kiểu object cho hàm thì const được sử dụng nhiều. Ví dụ như bạn cần trả lại một object TStrings. Chúng ta không được viết
function ABC: TStrings;
begin
end;
Mà phải viết là
procedure ABC(const X: TStrings);
begin
end;
Tuy là dùng const nhưng vì là object nên sẽ bị thay đổi. Với lại, truyền object bằng const sẽ có tốc độ nhanh hơn.
Out thì chỉ dùng để đưa giá trị ra, nó có thể bị hàm thay đổi. Out tương đương với Var nhưng đối với các kiểu như interface, string hoặc các kiểu có cấu trúc khác, tham số out sẽ đặt lại giá trị rỗng (vì out chỉ để đưa ra thôi, cần giá trị vào làm gì).
procedure Foo(out S: string);
begin
writeln(S); // Chẳng in ra gì cả, vì S bị reset thành chuỗi rỗng
S := 'ABC';
end;
var
S: string;
begin
S := 'XYZ';
Foo(S); // Truyền XYZ vào tham số out
writeln(S); // In ra ABC
end.
Đối với các kiểu như string và các kiểu có cấu trúc, thì tham số out bị reset. Tuy nhiên, các tham số out có kiểu như Integer, Byte, Boolean, ... sẽ không bị reset (giống như var)