Các biến số có nên sống trong phạm vi nhỏ nhất có thể có bao gồm các trường hợp các biến số không nên tồn tại nếu các biến có thể có không?


32

Theo câu trả lời được chấp nhận về "Cơ sở lý luận để thích các biến cục bộ hơn các biến thể hiện? ", Các biến nên sống trong phạm vi nhỏ nhất có thể.
Đơn giản hóa vấn đề theo cách hiểu của tôi, điều đó có nghĩa là chúng ta nên cấu trúc lại loại mã này:

public class Main {
    private A a;
    private B b;

    public ABResult getResult() {
        getA();
        getB();
        return ABFactory.mix(a, b);
    }

    private getA() {
      a = SomeFactory.getA();
    }

    private getB() {
      b = SomeFactory.getB();
    }
}

vào một cái gì đó như thế này:

public class Main {
    public ABResult getResult() {
        A a = getA();
        B b = getB();
        return ABFactory.mix(a, b);
    }

    private getA() {
      return SomeFactory.getA();
    }

    private getB() {
      return SomeFactory.getB();
    }
}

nhưng theo "tinh thần" của "các biến nên sống trong phạm vi nhỏ nhất có thể", "không bao giờ có biến" có phạm vi nhỏ hơn "có biến" không? Vì vậy, tôi nghĩ rằng phiên bản trên nên được cấu trúc lại:

public class Main {
    public ABResult getResult() {
        return ABFactory.mix(getA(), getB());
    }

    private getA() {
      return SomeFactory.getA();
    }

    private getB() {
      return SomeFactory.getB();
    }
}

do đó getResult()không có bất kỳ biến cục bộ nào cả. Điều đó có đúng không?


72
Tạo các biến rõ ràng đi kèm với lợi ích của việc phải đặt tên cho chúng. Giới thiệu một vài biến có thể nhanh chóng biến một phương thức mờ thành một phương thức có thể đọc được.
Jared Goguen

11
Thành thật mà nói, một ví dụ về tên biến a và b quá hạn chế để chứng minh giá trị của biến cục bộ. May mắn thay, bạn đã nhận được câu trả lời tốt.
Doc Brown

3
Trong đoạn mã thứ hai của bạn, các biến của bạn không thực sự là biến . Chúng là các hằng số cục bộ hiệu quả, bởi vì bạn không sửa đổi chúng. Trình biên dịch Java sẽ đối xử với chúng giống nhau nếu bạn định nghĩa chúng cuối cùng, bởi vì chúng nằm trong phạm vi cục bộ và trình biên dịch biết điều gì sẽ xảy ra với chúng. Đó là vấn đề về phong cách và quan điểm, nếu bạn thực sự nên sử dụng finaltừ khóa hay không.
hyde

2
@JaredGoguen Tạo các biến rõ ràng đi kèm với gánh nặng là phải đặt tên cho chúng và lợi ích của việc có thể đặt tên cho chúng.
Bergi

2
Hoàn toàn không có quy tắc kiểu mã như "các biến nên sống trong phạm vi nhỏ nhất có thể" được áp dụng phổ biến mà không cần suy nghĩ. Như vậy, bạn không bao giờ muốn đưa ra quyết định kiểu mã cho lý do của biểu mẫu trong câu hỏi này, trong đó bạn thực hiện một hướng dẫn đơn giản và mở rộng nó sang một khu vực ít được bao phủ rõ ràng ("các biến nên có phạm vi nhỏ nếu có thể" -> "Các biến không nên tồn tại nếu có thể"). Có những lý do chính đáng ủng hộ kết luận của bạn một cách cụ thể, hoặc kết luận của bạn là một phần mở rộng không phù hợp; một trong hai cách bạn không cần hướng dẫn ban đầu.
Ben

Câu trả lời:


110

Không. Có một số lý do tại sao:

  1. Các biến có tên có ý nghĩa có thể làm cho mã dễ hiểu hơn.
  2. Việc chia các công thức phức tạp thành các bước nhỏ hơn có thể giúp mã dễ đọc hơn.
  3. Bộ nhớ đệm.
  4. Giữ các tham chiếu đến các đối tượng để chúng có thể được sử dụng nhiều lần.

Và như vậy.


22
Cũng đáng đề cập: Giá trị sẽ được lưu trữ trong bộ nhớ bất kể, vì vậy nó thực sự kết thúc với cùng một phạm vi. Cũng có thể đặt tên cho nó (vì những lý do Robert đề cập ở trên)!
Có lẽ là

5
@Maybe_Factor Hướng dẫn không thực sự có vì lý do hiệu suất; đó là về cách dễ dàng để duy trì mã. Nhưng điều đó vẫn có nghĩa là người dân địa phương có ý nghĩa tốt hơn một biểu thức lớn, trừ khi bạn chỉ sáng tác các chức năng được đặt tên và thiết kế tốt (ví dụ: không có lý do gì để làm var taxIndex = getTaxIndex();).
Luaan

16
Tất cả đều là yếu tố quan trọng. Cách tôi nhìn vào nó: sự ngắn gọn là một mục tiêu; sự rõ ràng là khác; mạnh mẽ là khác; hiệu suất là khác; vân vân Không ai trong số này là mục tiêu tuyệt đối , và đôi khi chúng xung đột. Vì vậy, công việc của chúng tôi là tìm ra cách tốt nhất để cân bằng các mục tiêu đó.
chơi

8
Trình biên dịch sẽ chăm sóc 3 & 4.
Ngừng làm hại Monica

9
Số 4 có thể quan trọng cho sự chính xác. Nếu giá trị của một lệnh gọi hàm có thể thay đổi (ví dụ now():) loại bỏ biến và gọi phương thức nhiều lần có thể dẫn đến lỗi. Điều này có thể tạo ra một tình huống thực sự tinh tế và khó gỡ lỗi. Điều này có vẻ hiển nhiên nhưng nếu bạn đang thực hiện một nhiệm vụ tái cấu trúc mù để loại bỏ các biến, thì thật dễ dàng để kết thúc việc giới thiệu sai sót.
JimmyJames

16

Đồng ý, nên tránh các biến không cần thiết và không cải thiện khả năng đọc của mã. Càng nhiều biến trong phạm vi tại một điểm nhất định trong mã, mã đó càng phức tạp để hiểu.

Tôi thực sự không thấy lợi ích của các biến abtrong ví dụ của bạn, vì vậy tôi sẽ viết phiên bản không có biến. Mặt khác, chức năng rất đơn giản ở nơi đầu tiên tôi không nghĩ nó quan trọng lắm.

Nó trở thành một vấn đề nhiều hơn khi hàm nhận được càng lâu và càng có nhiều biến trong phạm vi.

Ví dụ nếu bạn có

    a=getA();
    b=getB();
    m = ABFactory.mix(a,b);

Ở đầu một hàm lớn hơn, bạn tăng gánh nặng tinh thần trong việc hiểu phần còn lại của mã bằng cách đưa ra ba biến chứ không phải một biến. Bạn có phải đọc qua phần còn lại của mã để xem nếu ahay bđược sử dụng một lần nữa. Các địa phương trong phạm vi dài hơn họ cần có hại cho khả năng đọc tổng thể.

Tất nhiên trong trường hợp một biến là cần thiết (ví dụ để lưu trữ một kết quả tạm thời) hoặc khi một biến không cải thiện khả năng đọc của mã, thì nó nên được giữ lại.


2
Tất nhiên, đến thời điểm một phương thức trở nên dài đến mức số lượng người địa phương quá lớn để có thể dễ dàng duy trì, bạn có thể muốn phá vỡ nó bằng mọi cách.
Luaan

4
Về lợi ích của các biến "ngoại lai" như var result = getResult(...); return result;là bạn có thể đặt một điểm dừng returnvà tìm hiểu chính xác resultnó là gì .
Joker_vD

5
@Joker_vD: Trình gỡ lỗi xứng đáng với tên của nó sẽ có thể cho phép bạn thực hiện tất cả những điều đó (xem kết quả của một cuộc gọi hàm mà không lưu trữ nó trong một biến cục bộ + đặt điểm dừng khi thoát hàm).
Christian Hackl

2
@ChristianHackl Rất ít debuggesr cho phép bạn làm điều đó mà không thiết lập đồng hồ phức tạp có thể mất thời gian để thêm vào (và nếu các chức năng có tác dụng phụ có thể phá vỡ mọi thứ). Tôi sẽ lấy phiên bản với các biến để gỡ lỗi bất kỳ ngày nào trong tuần.
Gabe Sượng

1
@ChristianHackl và để phát triển hơn nữa ngay cả khi trình gỡ lỗi không cho phép bạn làm điều đó theo mặc định vì trình gỡ lỗi được thiết kế khủng khiếp, bạn chỉ cần sửa đổi mã cục bộ trên máy của mình để lưu trữ kết quả và sau đó kiểm tra nó trong trình gỡ lỗi . Vẫn còn có lý do gì để lưu trữ một giá trị trong một biến cho chỉ mục đích đó.
Vịt lớn

11

Ngoài các câu trả lời khác, tôi muốn chỉ ra một điều khác. Lợi ích của việc giữ phạm vi của một biến nhỏ không chỉ là giảm bao nhiêu mã về mặt cú pháp có quyền truy cập vào biến mà còn giảm số lượng đường dẫn luồng điều khiển có thể có khả năng sửa đổi một biến (bằng cách gán cho nó một giá trị mới hoặc gọi một phương thức đột biến trên đối tượng hiện có được giữ trong biến).

Các biến trong phạm vi lớp (thể hiện hoặc tĩnh) có các đường dẫn luồng điều khiển có thể nhiều hơn đáng kể so với các biến trong phạm vi cục bộ bởi vì chúng có thể được thay đổi bằng các phương thức, có thể được gọi theo bất kỳ thứ tự nào, bất kỳ số lần nào và thường bằng mã bên ngoài lớp .

Hãy xem getResultphương pháp ban đầu của bạn :

public ABResult getResult() {
    getA();
    getB();
    return ABFactory.mix(this.a, this.b);
}

Bây giờ, tên getAgetBcó thể gợi ý rằng họ sẽ gán cho this.athis.b, chúng tôi không thể biết chắc chắn chỉ từ việc nhìn vào getResult. Do đó, có thể các giá trị this.athis.bđược truyền vào mixphương thức thay vì xuất phát từ trạng thái của thisđối tượng từ trước getResultđược gọi, điều này là không thể dự đoán được vì các máy khách kiểm soát cách thức và thời điểm được gọi.

Trong mã sửa đổi với các biến cục bộ abbiến, rõ ràng có chính xác một luồng điều khiển (không có ngoại lệ) từ việc gán từng biến cho việc sử dụng nó, bởi vì các biến được khai báo ngay trước khi chúng được sử dụng.

Do đó, có một lợi ích đáng kể khi di chuyển các biến (có thể sửa đổi) từ phạm vi lớp sang phạm vi cục bộ (cũng như di chuyển (có thể sửa đổi) từ bên ngoài vòng lặp sang bên trong) để đơn giản hóa lý do dòng điều khiển.

Mặt khác, việc loại bỏ các biến như trong ví dụ trước của bạn có ít lợi ích hơn, bởi vì nó không thực sự ảnh hưởng đến lý luận dòng điều khiển. Bạn cũng mất các tên được đặt cho các giá trị, điều này không xảy ra khi chỉ cần di chuyển một biến sang phạm vi bên trong. Đây là một sự đánh đổi mà bạn phải xem xét, vì vậy việc loại bỏ các biến có thể tốt hơn trong một số trường hợp và tồi tệ hơn ở những trường hợp khác.

Nếu bạn không muốn mất tên biến, nhưng vẫn muốn giảm phạm vi của các biến (trong trường hợp chúng được sử dụng bên trong hàm lớn hơn), bạn có thể xem xét gói các biến và cách sử dụng chúng trong câu lệnh khối ( hoặc di chuyển chúng đến chức năng riêng của chúng ).


2

Điều này hơi phụ thuộc vào ngôn ngữ, nhưng tôi muốn nói rằng một trong những lợi ích ít rõ ràng hơn của lập trình chức năng là nó khuyến khích người lập trình và người đọc mã không cần những thứ này. Xem xét:

(reduce (fn [map string] (assoc map string (inc (map string 0))) 

Hoặc một số LINQ:

var query2 = mydb.MyEntity.Select(x => x.SomeProp).AsEnumerable().Where(x => x == "Prop");

Hoặc Node.js:

 return visionFetchLabels(storageUri)
    .then(any(isCat))
    .then(notifySlack(secrets.slackWebhook, `A cat was posted: ${storageUri}`))
    .then(logSuccess, logError)
    .then(callback)

Cuối cùng là một chuỗi gọi một hàm trên kết quả của hàm trước đó, không có bất kỳ biến trung gian nào. Giới thiệu chúng sẽ làm cho nó ít rõ ràng hơn nhiều.

Tuy nhiên, sự khác biệt giữa ví dụ đầu tiên và hai ví dụ còn lại là thứ tự hoạt động ngụ ý . Điều này có thể không giống với thứ tự nó thực sự được tính toán, nhưng đó là thứ tự mà người đọc nên nghĩ về nó. Đối với hai thứ hai, điều này là trái sang phải. Đối với ví dụ Lisp / Clojure, nó giống từ phải sang trái hơn. Bạn nên cảnh giác một chút về việc viết mã không theo "hướng mặc định" cho ngôn ngữ của bạn và chắc chắn nên tránh các biểu thức "trung gian" kết hợp cả hai.

Toán tử đường ống của F # |>là hữu ích một phần vì nó cho phép bạn viết những thứ từ trái sang phải mà nếu không phải từ phải sang trái.


2
Tôi đã sử dụng dấu gạch dưới làm tên biến của mình trong các biểu thức LINQ hoặc lambdas đơn giản. myCollection.Select(_ => _.SomeProp).Where(_ => _.Size > 4);
Graham

Không chắc chắn câu trả lời này của OP trong một chút ...
AC

1
Tại sao các downvote? Có vẻ như là một câu trả lời tốt cho tôi, ngay cả khi nó không trả lời trực tiếp câu hỏi đó vẫn là thông tin hữu ích
reggaeg Ức

1

Tôi sẽ nói không, bởi vì bạn nên đọc "phạm vi nhỏ nhất có thể" là "trong số các phạm vi hiện có hoặc phạm vi hợp lý để thêm". Mặt khác, điều đó có nghĩa là bạn nên tạo phạm vi nhân tạo (ví dụ: {}các khối vô cớ trong các ngôn ngữ giống như C) chỉ để đảm bảo rằng phạm vi của một biến không vượt quá mục đích sử dụng cuối cùng, và điều đó thường sẽ được tán thành như là sự xáo trộn / lộn xộn trừ khi đã có một lý do tốt cho phạm vi tồn tại độc lập.


2
Một vấn đề với phạm vi trong nhiều ngôn ngữ là việc có một số biến có cách sử dụng cuối cùng là tính toán các giá trị ban đầu của các biến khác là cực kỳ phổ biến. Nếu một ngôn ngữ có cấu trúc rõ ràng cho mục đích phạm vi, cho phép các giá trị được tính toán sử dụng các biến phạm vi bên trong được sử dụng để khởi tạo các phạm vi ngoài phạm vi, thì các phạm vi đó có thể hữu ích hơn các phạm vi lồng nhau nghiêm ngặt mà nhiều ngôn ngữ yêu cầu.
supercat

1

Xem xét các hàm ( phương thức ). Ở đó, nó không tách mã trong các nhóm con nhỏ nhất có thể, cũng không phải là đoạn mã đơn lẻ lớn nhất.

Đó là một giới hạn thay đổi, với việc phân định các nhiệm vụ logic, thành các phần tiêu hao.

Các biến tương tự giữ cho các biến . Chỉ ra các cấu trúc dữ liệu logic, thành các phần dễ hiểu. Hoặc cũng chỉ đơn giản là đặt tên (nêu) các tham số:

boolean automatic = true;
importFile(file, automatic);

Nhưng tất nhiên có một tuyên bố ở đầu, và hai trăm dòng tiếp theo, việc sử dụng đầu tiên ngày nay được chấp nhận là phong cách xấu. Đây rõ ràng là những gì "các biến nên sống trong phạm vi nhỏ nhất có thể" dự định nói. Giống như rất gần "không sử dụng lại các biến."


1

Điều hơi thiếu vì lý do cho NO là gỡ lỗi / sẵn sàng. Mã phải được tối ưu hóa cho điều đó, và tên rõ ràng và súc tích giúp ích rất nhiều, ví dụ như tưởng tượng 3 cách nếu

if (frobnicate(x) && (get_age(x) > 2000 || calculate_duration(x) < 100 )

dòng này ngắn, nhưng đã khó đọc. Thêm một vài tham số, và nếu kéo dài nhiều dòng.

can_be_frobnicated = frobnicate(x)
is_short_lived_or_ancient = get_age(x) > 2000 || calculate_duration(x) < 100
if (can_be_frobnicated || is_short_lived_or_ancient )

Tôi thấy cách này dễ đọc hơn và để truyền đạt ý nghĩa - vì vậy tôi không có vấn đề gì với các biến trung gian.

Một ví dụ khác là các ngôn ngữ như R, trong đó dòng cuối cùng tự động là giá trị trả về:

some_func <- function(x) {
    compute(x)
}

Điều này là nguy hiểm, sự trở lại được giải thích hay cần thiết? Điều này rõ ràng hơn:

some_func <- function(x) {
   rv <- compute(x)
   return(rv)
}

Như mọi khi, đây là một cuộc gọi phán xét - loại bỏ các biến trung gian nếu chúng không cải thiện việc đọc, nếu không thì giữ hoặc giới thiệu chúng.

Một điểm khác có thể là khả năng sửa lỗi: Nếu kết quả trung gian được quan tâm, tốt nhất có thể đơn giản là giới thiệu một trung gian, như trong ví dụ R ở trên. Mức độ thường xuyên được gọi là khó tưởng tượng và cẩn thận những gì bạn đăng ký - quá nhiều biến gỡ lỗi gây nhầm lẫn - một lần nữa, một cuộc gọi phán xét.


0

Đề cập đến chỉ tiêu đề của bạn: hoàn toàn, nếu một biến là không cần thiết, nó sẽ bị xóa.

Nhưng không cần thiết, không có nghĩa là một chương trình tương đương có thể được viết mà không sử dụng biến, nếu không chúng ta sẽ được bảo rằng chúng ta nên viết mọi thứ trong hệ nhị phân.

Loại biến không cần thiết phổ biến nhất là biến không sử dụng, phạm vi của biến càng nhỏ thì càng dễ xác định rằng nó không cần thiết. Cho dù một biến trung gian là không cần thiết thì khó xác định hơn, bởi vì đó không phải là tình huống nhị phân, đó là theo ngữ cảnh. Trong thực tế, mã nguồn giống hệt nhau theo hai phương pháp khác nhau có thể tạo ra một câu trả lời khác nhau bởi cùng một người dùng tùy thuộc vào kinh nghiệm sửa chữa các vấn đề trong mã xung quanh trong quá khứ.

Nếu mã ví dụ của bạn chính xác như được trình bày, tôi sẽ khuyên bạn nên loại bỏ hai phương thức riêng tư, nhưng sẽ không có chút lo ngại nào về việc bạn đã lưu kết quả của các cuộc gọi của nhà máy vào một biến cục bộ hay chỉ sử dụng chúng làm đối số cho hỗn hợp phương pháp.

Khả năng đọc mã vượt qua mọi thứ trừ khi hoạt động chính xác (bao gồm chính xác các tiêu chí hiệu suất có thể chấp nhận được, điều này hiếm khi có thể nhanh nhất có thể).

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.