Tôi chưa hiểu rõ khái niệm về tác dụng phụ.
- Tác dụng phụ trong lập trình là gì?
- Có phải nó phụ thuộc ngôn ngữ lập trình?
- Có một điều như tác dụng phụ bên ngoài và bên trong?
Vui lòng cho một số ví dụ về nguyên nhân tạo ra tác dụng phụ.
Tôi chưa hiểu rõ khái niệm về tác dụng phụ.
Vui lòng cho một số ví dụ về nguyên nhân tạo ra tác dụng phụ.
Câu trả lời:
Một tác dụng phụ chỉ đơn giản là sửa đổi một số loại trạng thái - ví dụ:
Trái với những gì một số người dường như đang nói:
Một tác dụng phụ không phải bị ẩn hoặc bất ngờ (có thể, nhưng điều đó không liên quan gì đến định nghĩa vì nó áp dụng cho khoa học máy tính);
Một tác dụng phụ không có gì để làm với idempotency. Hàm idempotent có thể có tác dụng phụ và hàm không idempotent có thể không có tác dụng phụ (chẳng hạn như lấy ngày và giờ hệ thống hiện tại).
Nó thực sự rất đơn giản. Tác dụng phụ = thay đổi một cái gì đó ở đâu đó.
PS Như nhà bình luận benjol chỉ ra, một số người có thể nhầm lẫn giữa định nghĩa về tác dụng phụ với định nghĩa của hàm thuần túy , đó là một hàm (a) idempotent và (b) không có tác dụng phụ. Một cái không bao hàm cái khác trong khoa học máy tính nói chung, nhưng các ngôn ngữ lập trình chức năng thường sẽ có xu hướng thực thi cả hai ràng buộc.
++a
.. Không giống như sự phân công. b = ++a;
có hai tác dụng phụ. Một điều hiển nhiên và sự phân công của tiền điện tử a
. Đó là loại điều mà một tác dụng phụ mà (với một số) là mong muốn. Nhưng đã được gọi là một tác dụng phụ cho toàn bộ sự nghiệp của tôi để làm cho nó không tinh tế.
Bất kỳ hoạt động nào sửa đổi trạng thái của máy tính hoặc tương tác với thế giới bên ngoài được cho là có tác dụng phụ. Xem Wikipedia về Tác dụng phụ .
Ví dụ, chức năng này không có tác dụng phụ. Kết quả của nó chỉ phụ thuộc vào các đối số đầu vào của nó và không có gì về trạng thái của chương trình hoặc môi trường của nó thay đổi khi được gọi:
int square(int x) { return x * x; }
Ngược lại, việc gọi các chức năng này sẽ cung cấp cho bạn các kết quả khác nhau tùy theo thứ tự bạn gọi chúng, bởi vì chúng thay đổi điều gì đó về trạng thái của máy tính:
int n = 0;
int next_n() { return n++; }
void set_n(int newN) { n = newN; }
Hàm này có tác dụng phụ là ghi dữ liệu vào đầu ra. Bạn không gọi hàm vì bạn muốn giá trị trả về của nó; bạn gọi nó bởi vì bạn muốn hiệu ứng của nó đối với "thế giới bên ngoài":
int Write(const char* s) { return printf("Output: %s\n", s); }
Write
ví dụ của bạn chứng minh, có tác dụng phụ không có nghĩa là hàm bao giờ thay đổi đầu ra của nó đối với đầu vào của nó, hoặc thậm chí đầu ra của nó phụ thuộc vào đầu vào.
square(x)
có thể khiến mô-đun nơi hàm được xác định được tải từ đĩa. Điều này có nên được coi là tác dụng phụ? Rốt cuộc, người đàn ông mà cuộc gọi (đầu tiên) này mất nhiều thời gian bất ngờ, việc sử dụng RAM tăng lên, v.v.
square(x)
vì bạn muốn trạng thái máy tính bên ngoài thay đổi, bạn có thể coi đó là một tác dụng phụ.
Tôi nghĩ rằng các câu trả lời hiện có là khá tốt. Tôi muốn giải thích một số khía cạnh mà IMO chưa được nhấn mạnh đủ.
Trong toán học, một hàm chỉ là một ánh xạ từ một bộ giá trị đến một giá trị. Vì vậy, được đưa ra một hàm f
và một giá trị x
, f(x)
sẽ luôn luôn là kết quả tương tự y
. Bạn cũng có thể thay thế f(x)
bằng y
mọi nơi trong một biểu thức và sẽ không có gì thay đổi.
Cái được gọi là hàm (hoặc thủ tục) trong nhiều ngôn ngữ lập trình là cấu trúc (đoạn mã) có thể được thực thi vì:
Vì vậy, các hiệu ứng có thể liên quan đến trạng thái mà còn liên quan đến các khía cạnh khác như bắn tên lửa hoặc tạm dừng thực thi trong vài giây.
Thuật ngữ tác dụng phụ có thể nghe có vẻ tiêu cực nhưng thông thường, hiệu ứng của việc gọi một chức năng là mục đích của chính chức năng đó. Tôi cho rằng, vì thuật ngữ hàm ban đầu được sử dụng trong Toán học, tính toán một giá trị được coi là hiệu ứng chính của hàm trong khi mọi hiệu ứng khác được coi là tác dụng phụ . Một số ngôn ngữ lập trình sử dụng thủ tục thuật ngữ để tránh nhầm lẫn với các hàm theo nghĩa toán học.
Lưu ý rằng
sleep()
trong Python, chỉ hữu ích cho các hiệu ứng (phụ) của chúng ,. Chúng thường được mô hình hóa như các hàm trả về một giá trị đặc biệt None
, unit
hoặc ()
hoặc ..., chỉ đơn giản chỉ ra rằng tính toán đã kết thúc chính xác.Một tác dụng phụ là khi một hoạt động có ảnh hưởng đến một biến / đối tượng nằm ngoài mục đích sử dụng.
Nó có thể xảy ra khi bạn thực hiện cuộc gọi đến một hàm phức tạp có tác dụng phụ là thay đổi một số biến toàn cục, mặc dù đó không phải là lý do bạn gọi nó (có thể bạn đã gọi nó để trích xuất một cái gì đó từ cơ sở dữ liệu).
Tôi thừa nhận rằng tôi gặp khó khăn khi đưa ra một ví dụ đơn giản mà trông không hoàn toàn bị chiếm đoạt, và các ví dụ từ những thứ tôi đã làm việc quá lâu để đăng ở đây (và vì nó liên quan đến công việc, nên có lẽ tôi không nên ).
Một ví dụ tôi đã thấy (cách đây một thời gian) là một chức năng mở kết nối cơ sở dữ liệu nếu kết nối ở trạng thái đóng. Vấn đề là nó được cho là đóng kết nối ở cuối chức năng, nhưng nhà phát triển đã quên thêm mã đó. Vì vậy, ở đây, có một tác dụng phụ ngoài ý muốn : gọi một thủ tục được cho là chỉ thực hiện một truy vấn và tác dụng phụ là kết nối vẫn mở và nếu chức năng được gọi hai lần liên tiếp, một lỗi sẽ xuất hiện cho biết kết nối là đã mở
Ok, vì bây giờ mọi người đang đưa ra ví dụ, tôi nghĩ tôi cũng sẽ như vậy;)
/*code is PL/SQL-styled pseudo-code because that's what's on my mind right now*/
g_some_global int := 0; --define a globally accessible variable somewhere.
function do_task_x(in_a in number) is
begin
b := calculate_magic(in_a);
if b mod 2 == 0 then
g_some_global := g_some_global + b;
end if;
return (b * 2.3);
end;
Các chức năng do_task_x
có chính hiệu lực thi hành trở về kết quả của một số tính toán, và một bên ảnh hưởng của thể sửa đổi một biến toàn cầu.
Tất nhiên, đó là chính và đó là tác dụng phụ có thể được mở để giải thích và có thể phụ thuộc vào việc sử dụng thực tế. Nếu tôi gọi hàm này cho mục đích sửa đổi toàn cục và tôi loại bỏ giá trị trả về hơn tôi nói rằng sửa đổi toàn cục là hiệu ứng chính.
Trong khoa học máy tính, một chức năng hoặc biểu thức được cho là có tác dụng phụ nếu nó sửa đổi một số trạng thái hoặc có tương tác quan sát được với các chức năng gọi hoặc thế giới bên ngoài.
Một hàm, theo nghĩa toán học, là một ánh xạ từ đầu vào đến đầu ra. Hiệu quả dự định của việc gọi một hàm là để nó ánh xạ đầu vào tới đầu ra mà nó trả về. Nếu hàm làm bất cứ điều gì khác, nó không thành vấn đề, nhưng nếu nó có bất kỳ hành vi nào không ánh xạ đầu vào vào đầu ra, thì hành vi đó được biết là một tác dụng phụ.
Nói một cách tổng quát hơn, một hiệu ứng phụ là bất kỳ hiệu ứng nào không phải là hiệu ứng dự định của người thiết kế công trình.
Một hiệu ứng là bất cứ điều gì ảnh hưởng đến một diễn viên. Nếu tôi gọi một chức năng gửi cho bạn gái của tôi một tin nhắn văn bản chia tay, điều đó ảnh hưởng đến một loạt các diễn viên, tôi, cô ấy, mạng của công ty điện thoại di động, v.v. để trả lại cho tôi một ánh xạ từ đầu vào của tôi. Vì vậy đối với:
public void SendBreakupTextMessage() {
Messaging.send("I'm breaking up with you!")
}
Nếu điều này được dự định là một hàm, thì điều duy nhất nó nên làm là trả về void. Nếu nó là hiệu ứng phụ miễn phí, nó thực sự không nên gửi tin nhắn văn bản.
Trong hầu hết các ngôn ngữ lập trình, không có cấu trúc cho một hàm toán học. Không có cấu trúc được dự định sẽ được sử dụng như vậy. Đó là lý do tại sao hầu hết các ngôn ngữ nói rằng bạn có phương pháp hoặc thủ tục. Theo thiết kế, chúng được dự định để có thể làm nhiều hiệu ứng hơn. Theo cách nói lập trình thông thường, không ai thực sự quan tâm đến ý định của phương thức hay thủ tục là gì, vì vậy khi ai đó nói hàm này có tác dụng phụ, nghĩa là, cấu trúc này không hoạt động như một hàm toán học. Và khi ai đó nói rằng hàm này không có tác dụng phụ, ý họ là, cấu trúc này hoạt động hiệu quả như một hàm toán học.
Theo định nghĩa, một hàm thuần túy luôn có tác dụng phụ miễn phí. Hàm thuần túy, là một cách để nói, hàm này, mặc dù nó sử dụng một cấu trúc cho phép nhiều hiệu ứng hơn, nhưng chỉ có tác dụng tương đương với hàm của hàm toán học.
Tôi thách thức bất cứ ai nói với tôi khi chức năng miễn phí tác dụng phụ sẽ không thuần túy. Trừ khi hiệu ứng dự định chính của ngữ cảnh của câu sử dụng thuật ngữ thuần túy và hiệu ứng phụ miễn phí không phải là hiệu ứng dự định toán học của một hàm, thì những cái đó luôn luôn bằng nhau.
Như vậy, đôi khi, mặc dù hiếm hơn, và tôi tin rằng đây là sự khác biệt thiếu và cũng là hiểu lầm mọi người (vì đó không phải là giả định phổ biến nhất) trong câu trả lời được chấp nhận, nhưng đôi khi người ta cho rằng tác dụng dự định của chức năng lập trình là để ánh xạ đầu vào thành đầu ra, trong đó đầu vào không bị ràng buộc với các tham số rõ ràng của hàm, nhưng đầu ra bị ràng buộc với giá trị trả về rõ ràng. Nếu bạn cho rằng đó là hiệu ứng mong muốn, thì một hàm đọc tệp và trả về một kết quả khác dựa trên những gì trong tệp vẫn không có tác dụng phụ, vì bạn đã cho phép các đầu vào đến từ những nơi khác trong hiệu ứng dự định của bạn.
Vì vậy, tại sao tất cả điều này quan trọng?
Đó là tất cả về kiểm soát và giữ nó. Nếu bạn gọi một hàm và nó làm một cái gì đó khác thì trả về một giá trị, thật khó để suy luận về hành vi của nó. Bạn sẽ cần phải nhìn vào bên trong hàm để tìm mã thực tế để đoán nó đang làm gì và khẳng định tính chính xác của nó. Tình huống lý tưởng là nó rất rõ ràng và dễ dàng để biết đầu vào mà hàm đang sử dụng là gì và nó không làm gì khác sau đó trả lại một đầu ra cho nó. Bạn có thể thư giãn điều này một chút và nói rằng biết chính xác đầu vào mà nó đang sử dụng không hữu ích như chắc chắn nó không làm gì khác mà bạn có thể không biết sau đó trả về một giá trị, vì vậy có thể bạn hài lòng với việc chỉ thực thi rằng nó không làm gì khác sau đó ánh xạ đầu vào, bất kể nó lấy từ đâu, đến đầu ra.
Trong hầu hết các trường hợp, quan điểm của một chương trình là có các hiệu ứng khác sau đó ánh xạ mọi thứ đi vào những thứ sắp ra. Ý tưởng kiểm soát hiệu ứng phụ là bạn có thể tổ chức mã theo cách dễ hiểu và lý do hơn. Nếu bạn đặt tất cả các tác dụng phụ lại với nhau, ở một nơi rất rõ ràng và trung tâm, bạn sẽ dễ dàng biết nơi để tìm và tin rằng đây là tất cả những gì đang xảy ra, không còn nữa. Nếu bạn cũng có đầu vào rất rõ ràng, nó sẽ giúp kiểm tra hành vi cho các đầu vào khác nhau và nó dễ sử dụng hơn, vì bạn không cần thay đổi đầu vào ở nhiều nơi khác nhau, một số có thể không rõ ràng, chỉ là để có được những gì bạn muốn.
Bởi vì điều hữu ích nhất để hiểu, lý do và kiểm soát hành vi của một chương trình là để tất cả các đầu vào được nhóm lại rõ ràng với nhau và rõ ràng, cũng như tất cả các tác dụng phụ được nhóm lại với nhau và rõ ràng, đây thường là những gì mọi người nói về khi họ nói tác dụng phụ, tinh khiết, vv
Bởi vì hữu ích nhất là nhóm các tác dụng phụ và nhân chứng của họ, đôi khi mọi người sẽ chỉ có nghĩa như vậy, và phân biệt nó bằng cách nói rằng nó không thuần túy, nhưng vẫn "không có tác dụng phụ". Nhưng tác dụng phụ có liên quan đến "hiệu ứng chính dự định" giả định, vì vậy đó là một thuật ngữ theo ngữ cảnh. Điều này tôi thấy ít được sử dụng, mặc dù đáng ngạc nhiên là nó được nói đến rất nhiều trong chủ đề này.
Cuối cùng, idempotent có nghĩa là gọi hàm này nhiều lần với cùng một đầu vào (không quan trọng chúng đến từ đâu) sẽ luôn dẫn đến các hiệu ứng tương tự (tác dụng phụ hay không).
Trong lập trình, tác dụng phụ là khi một thủ tục thay đổi một biến từ bên ngoài phạm vi của nó. Tác dụng phụ không phụ thuộc vào ngôn ngữ. Có một số loại ngôn ngữ nhằm loại bỏ tác dụng phụ (ngôn ngữ chức năng thuần túy), nhưng tôi không chắc có ngôn ngữ nào yêu cầu tác dụng phụ hay không, nhưng tôi có thể sai.
Theo tôi biết, không có tác dụng phụ bên trong và bên ngoài.
Đây là một ví dụ đơn giản:
int _totalWrites;
void Write(string message)
{
// Invoking this function has the side effect of
// incrementing the value of _totalWrites.
_totalWrites++;
Debug.Write(message);
}
Định nghĩa về tác dụng phụ không đặc trưng cho lập trình nên chỉ cần tưởng tượng các tác dụng phụ của thuốc hoặc ăn quá nhiều thực phẩm.
x++
sửa đổi biến x
thường được coi là một tác dụng phụ. Giá trị đó của biểu thức là giá trị tăng trước của x
; đây là phần hiệu ứng không phụ của biểu thức.
Một tác dụng phụ là những điều xảy ra trong mã không rõ ràng.
Ví dụ: giả sử bạn có lớp này
public class ContrivedRandomGenerator {
public int Seed { get; set; }
public int GetRandomValue()
{
Random(Seed);
Seed++;
}
}
Khi bạn ban đầu tạo lớp, bạn cung cấp cho nó một hạt giống.
var randomGenerator = new ContrivedRandomGenerator();
randomGenerator.Seed = 15;
randomGenerator.GetRandomValue();
Bạn không biết nội bộ, bạn chỉ mong nhận được một giá trị ngẫu nhiên và bạn sẽ mong đợi RandomGenerator. Vẫn còn 15 ... nhưng thực tế không phải vậy.
Lệnh gọi có tác dụng phụ là thay đổi giá trị Seed.