Computer >> Máy Tính >  >> Lập trình >> Ruby

Hộp công cụ Rubys Bitwise:Toán tử, Ứng dụng và Trò ảo thuật

Bạn có thể dành cả đời để xây dựng các ứng dụng Rails và không bao giờ cần sử dụng bất kỳ toán tử bitwise nào. Nếu bạn là người mới lập trình, bạn thậm chí có thể không biết "bitwise" nghĩa là gì.

Nhưng thời điểm bạn quan tâm đến các hệ thống mà hiệu quả là quan trọng và tài nguyên có hạn, thì các hoạt động bitwise trở nên quan trọng. Chúng được sử dụng rộng rãi với những thứ như giao thức mạng, mật mã, quyền đối với tệp Unix và hệ thống nhúng.

Ngoài ra, để thực sự hiểu cách máy tính thực hiện những việc như cộng hai số, bạn phải hiểu các thao tác bitwise.

Vì Ruby và nhiều ngôn ngữ khác có hỗ trợ bản địa, chúng là một công cụ tuyệt vời để thêm vào hộp công cụ của bạn, ngay cả khi nó chỉ để hiểu những gì đang xảy ra nếu bạn gặp chúng trong tương lai.

Trong bài viết này, chúng ta sẽ đề cập đến các phép toán bitwise là gì, một số ví dụ về nơi chúng có thể được áp dụng và cách chúng ta có thể sử dụng chúng tốt nhất trong Ruby. Hãy bắt đầu.

Các phép toán bitwise là gì?

Ở mức thấp nhất trong bất kỳ máy tính nào, chúng ta chỉ có 1s và 0, còn được gọi là bit. Bất kỳ hoạt động nào khác mà chúng tôi sử dụng trong Ruby hoặc bất kỳ ngôn ngữ lập trình nào khác cuối cùng đều được lưu trữ dưới dạng số 1 và số 0, nhưng phần mềm trong máy tính của chúng tôi sẽ biến đổi chúng qua lại một cách hiệu quả giữa những gì chúng tôi thấy và những gì thực sự được lưu trữ. Ví dụ:chúng tôi lưu trữ chuỗi "hello" dưới dạng chuỗi 1 và 0.

Các phép toán bit cho phép chúng tôi truy cập trực tiếp các bit đó, thường là đại diện cho các số và làm "điều gì đó" với chúng. Một số thao tác sẽ giúp chúng tôi triển khai các tính năng trong phần mềm của mình theo cách thanh lịch và hiệu quả hơn.

Hiện tại, có hai khía cạnh quan trọng cần làm nổi bật về hoạt động bitwise:chúng cực kỳ hiệu quả trong việc lưu trữ thông tin, vì chúng tôi đang sử dụng trực tiếp các bit và chúng thực thi rất nhanh.

Hành vi cơ bản là có một tập hợp các bit và áp dụng toán tử cho nó. Bạn cũng có thể sử dụng các toán tử với hai bộ bit. Hãy xem một số thao tác:

KHÔNG hoạt động theo chiều bit

Đây là một phép toán đơn phân, có nghĩa là nó chỉ áp dụng cho một tập hợp các bit và nó đơn giản như thay thế các số 1 bằng các số 0 và ngược lại. Nó được biểu thị bằng ký hiệu:~ .

~101000 = 010111

VÀ thao tác theo chiều bit

AND là một phép toán áp dụng cho hai bộ bit; ký hiệu của nó là & và nó tuân theo logic sau:

1 & 1 = 1
1 & 0 = 0
0 & 1 = 0
0 & 0 = 0

Vì vậy, khi chúng ta có hai bộ bit có độ dài bằng nhau, kết quả chỉ là áp dụng logic này cho từng cặp riêng lẻ:

0110 AND
0111
-----
0110

Trong Ruby:

25.to_s(2)           # 11001
30.to_s(2)           # 11110
(25 & 30).to_s(2)    # 11000

HOẶC thao tác theo chiều bit

Tương tự như phép toán AND, một phép toán khác cũng áp dụng cho hai bộ bit là OR theo chiều bit. Ký hiệu của nó là | và tuân theo logic này:

1 | 1 = 1
1 | 0 = 1
0 | 1 = 1
0 | 0 = 0

Vì vậy, nếu chúng ta có 1 ở hai bên, kết quả là 1, ngược lại là 0. Rất đơn giản! Hãy xem một phép toán HOẶC với nhiều bit hơn:

0110 OR
0111
-----
0111

Và trong Ruby:

25.to_s(2)           # 11001
30.to_s(2)           # 11110
(25 | 30).to_s(2)    # 11111

Ví dụ thực tế 1:Quyền

Tại thời điểm này, bạn có thể tự hỏi tại sao các hoạt động này lại hữu ích. Rốt cuộc, chúng có vẻ khá thấp. Bạn có thể không có bất kỳ kế hoạch nào để làm việc trực tiếp với các giao thức mạng, đồ họa hoặc mật mã.

Nhưng bạn có thể đã làm việc với quyền trước đây. Quyền là một lĩnh vực mà các hoạt động bitwise có thể thực sự hữu ích. Hãy tưởng tượng bạn có một hệ thống quyền nơi người dùng có thể thực hiện các hành động khác nhau trong một tài liệu:

  • Xem
  • Chỉnh sửa
  • Xóa
  • Mời những người dùng khác

Và bây giờ chúng tôi muốn mô hình hóa các hành động này thành các vai trò khác nhau:

  • Trợ lý: có thể xem và chỉnh sửa tài liệu.
  • Người quan sát: chỉ có thể xem tài liệu.
  • Tác giả: có thể thực hiện tất cả các hành động.

Làm thế nào chúng ta có thể mô hình hóa điều này? Làm cách nào để chúng tôi biết được người dùng có một trong những vai trò này tại bất kỳ thời điểm nào hay không? Một câu trả lời là sử dụng các phép toán bitwise.

Chúng tôi chỉ lưu trữ một tập hợp các bit cho mỗi người dùng đại diện cho các quyền mà họ có:

(Starting from the right side)
1st bit: View
2nd bit: Edit
3rd bit: Delete
4th bit: Invite other users

Ví dụ:

0001 = Can only view the document.
0011 = Can view and edit the document.
1001 = Can view, can't edit, can't delete and can invite others.

Khi chúng tôi đã đặt các giá trị này cho một số người dùng, chúng tôi có thể thực hiện một số so sánh thực sự nhanh chóng với sự cho phép mà chúng tôi muốn. Hãy tưởng tượng chúng ta đang kiểm tra xem người dùng có thể chỉnh sửa tài liệu hay không. Chúng tôi có thể sử dụng phép toán AND bitwise:

# This is called a bit mask. It contains only the value we want to check, in this case the second bit for editing.
EDIT_PERMISSION_MASK = 0b0010

# We can define a quick method to check this: 
def can_edit_document?(user_permisions)
  (EDIT_PERMISSION_MASK & user_permisions) != 0
end

Điều này có nghĩa là nếu phép toán bitwise AND cho chúng ta một cái gì đó khác 0, chúng ta có bộ bit đó:

0010 AND
1101
----
0000 == 0 so we don't have the permission

0010 AND
1110
----
0010 != 0 so we have the permission

Chúng tôi có thể áp dụng logic tương tự cho bất kỳ quyền nào khác bằng cách sửa đổi vị trí của bit mà chúng tôi muốn kiểm tra, vì vậy chúng tôi sẽ kết thúc với các hằng số này và các phương thức tương ứng:

VIEW_PERMISSION_MASK   = 0b0001
EDIT_PERMISSION_MASK   = 0b0010
DELETE_PERMISSION_MASK = 0b0100
INVITE_PERMISSION_MASK = 0b1000

Ngoài ra, chúng tôi có thể xác định động các quyền và lưu trữ các quyền mới trong tương lai bằng cách kiểm tra bit nhanh.

Ví dụ:chúng tôi đã nói trước đó rằng một trợ lý chỉ có thể xem và chỉnh sửa tài liệu, vì vậy các quyền cho người dùng đó sẽ là:0011 . Chúng tôi có thể lưu trữ giá trị đó trong cơ sở dữ liệu của mình và sau đó chúng tôi có thể dễ dàng kiểm tra xem một trợ lý có thể thực hiện một hành động với các phương thức được xác định trước đó hay không.

ASSISTANT_MASK = VIEW_PERMISSION_MASK | EDIT_PERMISSION_MASK
# This will be: 0011

# Optionally, we could use this method to check if this user is an assistant. This method could be defined inside the User class.
def is_assistant?(user)
  (user.permissions == ASSISTANT_MASK)
end

Nếu tất cả những điều này nghe có vẻ quen thuộc, đó là bởi vì đó là cùng một cách tiếp cận thường được sử dụng cho các quyền đối với tệp trong các hệ thống dựa trên Unix.

Ví dụ thực tế 2:Các vị trí trong nhóm

Hãy sử dụng các phép toán bitwise nhiều hơn một chút 😉.

Chúng ta có thể áp dụng chúng trong một trường hợp khác tương đối phổ biến:các vị trí khác nhau trong một đội thể thao hoặc các vị trí công việc trong một công ty. Hãy đi cùng một đội bóng rổ để đơn giản hóa.

Trong bóng rổ, có 5 vị trí khi chơi một trò chơi:- Hộ vệ điểm - Hộ vệ sút - Tiến nhỏ - Tiến công lực - Trung tâm

Và chúng ta có thể chỉ định một tập hợp các bit cho mỗi vị trí: 00001 Point guard 00010 Shooting guard 00100 Small forward 01000 Power forward 10000 Center

Tương tự trong Ruby sẽ là:

POINT_GUARD_POSITION    = 0b00001
SHOOTING_GUARD_POSITION = 0b00010
SMALL_FORWARD_POSITION  = 0b00100
POWER_FORWARD_POSITION  = 0b01000
CENTER_POSITION         = 0b10000

POINT_GUARD_POSITION | SHOOTING_GUARD_POSITION | SMALL_FORWARD_POSITION | POWER_FORWARD_POSITION | CENTER_POSITION # = 31

Vì vậy, bây giờ chúng tôi có thể làm một số điều thú vị, chẳng hạn như kiểm tra xem cả nhóm có hiện diện với:

# p1...p5 are the positions of each player
is_full_team_present = (p1 | p2 | p3 | p4 | p5 == 31)

Tại sao? Bằng cách thực hiện phép toán OR theo bit, nếu chúng ta có tất cả các vị trí, chúng ta sẽ kết thúc bằng:11111.

# OR bitwise operation
00001 |
00010 |
00100 |
01000 |
10000
-----
11111

Và 11111 là 31, bởi vì 2 ^ 0 + 2 ^ 1 + 2 ^ 2 + 2 ^ 3 + 2 ^ 4 =31.

Điều này không liên quan chặt chẽ đến các hoạt động bitwise, nhưng với mô hình dữ liệu này, chúng tôi cũng có thể kiểm tra xem hai người chơi có thể được trao đổi hay không. Điều này sẽ rất đơn giản:

def can_be_exchanged?(player1, player2)
  player1.position == player2.position
end

XOR

Một thao tác khác mà chúng ta có thể thực hiện với hai bộ bit là XOR, có ký hiệu là ^ .

XOR có nghĩa là độc quyền HOẶC và nó tuân theo logic sau:

1 | 1 = 0
1 | 0 = 1
0 | 1 = 1
0 | 0 = 0

Vì vậy, kết quả sẽ chỉ là 1 nếu một trong hai bit là 1; nếu cả hai đều bằng nhau thì sẽ là 0.

Phép toán này được sử dụng trong một số thuật toán để so sánh một số với chính nó, vì x ^ x =0.

Ca

Đây là một nhóm hoạt động thú vị trong đó chúng ta "di chuyển" các bit sang bên này hoặc bên kia trong một tập hợp. Hãy để tôi giải thích.

Với một sự thay đổi bit, chúng tôi di chuyển hoặc "dịch chuyển" các bit theo một hướng, sang trái hoặc phải:

00010111 left-shift
<-------
00101110
10010111 right-shift
------->
11001011

Chúng ta có thể di chuyển hoặc dịch chuyển các bit n lần. Đây là một sự dịch chuyển sang trái được áp dụng cho số 5 hai lần trong Ruby:

5.to_s(2) # 101
(5 << 2).to_s(2) # 10100

Như bạn có thể thấy, sự dịch chuyển sang trái được biểu thị bằng <<. Một sự thay đổi đúng đắn sử dụng>>:

5.to_s(2) # 101
(5 >> 2).to_s(2) # 1

Trong trường hợp này, chúng tôi chỉ nhận được một vì các bit "0" và "1" từ 1 * 01 * đã bị loại bỏ.

Chia cho 2 bằng dịch phải

Một sự thật thú vị về sự thay đổi bit là bạn có thể tính toán một số phép toán với chúng. Trước đây, điều này nhanh hơn, nhưng ngày nay điều này hầu như chỉ được sử dụng bởi các lập trình viên, những người làm việc với nhiều ràng buộc hơn về tài nguyên như trong phát triển trò chơi.

Nếu chúng ta áp dụng dịch chuyển sang phải cho một số, chúng ta sẽ nhận được kết quả chia nó cho 2:

10.to_s(2)        # 1010
(10 >> 1).to_s(2) # 101
10 >> 1           # 5

Nhân với 2 với dịch chuyển trái

Theo cách tương tự, chúng ta có thể nhân với 2 bằng cách dịch chuyển sang trái:

10.to_s(2)        # 1010
(10 << 1).to_s(2) # 10100
10 << 1           # 20

Kiểm tra nhanh xem một số là lẻ hay chẵn

Có một ví dụ rất đơn giản về thao tác bitwise thực sự nhanh và dễ làm theo.

Hãy tưởng tượng chúng ta thực hiện một phép toán AND với 1, chỉ 1. Vì vậy, đó sẽ là bất kỳ số 0 nào, tùy thuộc vào máy tính chúng ta đang sử dụng và 1. Hãy làm điều đó với 2:

2 = 00000010 &
    00000001
-------------
    00000000

Bây giờ với 4:

4 = 00000100 &
    00000001
-------------
    00000000

Còn 5 thì sao?

5 = 00000101 &
    00000001
-------------
    00000001

Bây giờ chúng tôi có 1. Bạn có đoán được điều này có nghĩa là gì không?

Bằng cách thực hiện điều này VÀ với 1, nếu số chẵn, chúng ta sẽ nhận được 0. Nếu là số lẻ, chúng ta sẽ nhận được 1. Chúng ta có thể dễ dàng sử dụng thực tế này để tạo một vài phương thức trong Ruby:

def is_odd?(number)
  number & 1
end
def is_even?(number)
  is_odd?(number) == 0
end
# Or:
def is_even?(number)
  (number & 1) == 0
end

Nếu bạn muốn đi xa hơn hoặc trải nghiệm một số khoảnh khắc thú vị với bit, hãy xem bộ sưu tập hack bitwise này để biết thêm thủ thuật.

Kết luận

Các thao tác bitwise rất khó thực hiện nếu bạn chưa từng gặp phải, nhưng một khi bạn đã quen thuộc với chúng, bạn sẽ chuẩn bị tốt hơn để đối phó với các dự án hiện tại hoặc trong tương lai sử dụng chúng. Ngoài ra, bạn sẽ có một công cụ mới khi lập kế hoạch giải pháp cho một vấn đề trong mã của bạn.