Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

กล่องเครื่องมือ Rubys Bitwise:ตัวดำเนินการ แอปพลิเคชัน และเทคนิคมายากล

คุณอาจใช้เวลาทั้งชีวิตในการสร้างแอป Rails และไม่จำเป็นต้องใช้โอเปอเรเตอร์ระดับบิตเลย หากคุณเพิ่งเริ่มเขียนโปรแกรม คุณอาจไม่รู้ด้วยซ้ำว่า "ระดับบิต" หมายถึงอะไร

แต่ทันทีที่คุณสนใจระบบที่ประสิทธิภาพเป็นสิ่งสำคัญและทรัพยากรมีจำกัด การดำเนินการระดับบิตก็มีความสำคัญ มีการใช้อย่างกว้างขวางกับสิ่งต่างๆ เช่น โปรโตคอลเครือข่าย การเข้ารหัส การอนุญาตไฟล์ Unix และระบบฝังตัว

นอกจากนี้ เพื่อให้เข้าใจจริง ๆ ว่าคอมพิวเตอร์ทำการบวกเลขสองตัวอย่างไร คุณต้องเข้าใจการดำเนินการระดับบิต

เนื่องจาก Ruby และภาษาอื่นๆ มากมายมีการสนับสนุนแบบเนทีฟ พวกเขาจึงเป็นเครื่องมือที่ยอดเยี่ยมในการเพิ่มลงในกล่องเครื่องมือของคุณ แม้ว่าจะเป็นเพียงการทำความเข้าใจว่าจะเกิดอะไรขึ้นหากคุณพบพวกเขาในอนาคต

ในบทความนี้ เราจะพูดถึงว่าการดำเนินการระดับบิตคืออะไร ตัวอย่างบางส่วนของตำแหน่งที่สามารถนำไปใช้ และวิธีที่เราจะใช้ประโยชน์ได้ดีที่สุดใน Ruby เริ่มกันเลย

การดำเนินการระดับบิตคืออะไร

ที่ระดับต่ำสุดในคอมพิวเตอร์ทุกเครื่อง เรามีเพียง 1 และ 0 หรือที่เรียกว่าบิตเท่านั้น การดำเนินการอื่นใดที่เราใช้ในภาษา Ruby หรือภาษาโปรแกรมอื่น ๆ จะถูกจัดเก็บเป็น 1 และ 0 แต่ซอฟต์แวร์ในคอมพิวเตอร์ของเราจะแปลงไปมาระหว่างสิ่งที่เราเห็นกับสิ่งที่จัดเก็บจริงได้อย่างมีประสิทธิภาพ ตัวอย่างเช่น เราเก็บสตริง "hello" เป็นลูกโซ่ของ 1 และ 0

การดำเนินการระดับบิตทำให้เราสามารถเข้าถึงบิตเหล่านั้นได้โดยตรง ซึ่งมักจะใช้แทนตัวเลข และทำ "บางอย่าง" กับบิตเหล่านั้น การดำเนินการบางอย่างจะช่วยเราในการใช้คุณลักษณะต่างๆ ในซอฟต์แวร์ของเราในลักษณะที่หรูหราและมีประสิทธิภาพยิ่งขึ้น

มีสองประเด็นสำคัญที่ควรเน้นเกี่ยวกับการดำเนินการระดับบิตในตอนนี้:มีประสิทธิภาพอย่างมากในการจัดเก็บข้อมูล เนื่องจากเราใช้บิตโดยตรง และดำเนินการได้รวดเร็วมาก

ลักษณะการทำงานพื้นฐานคือการมีชุดของบิตและนำตัวดำเนินการไปใช้กับมัน คุณยังสามารถใช้ตัวดำเนินการกับบิตสองชุดได้ มาดูการดำเนินงานกันบ้าง:

ไม่ใช่การทำงานระดับบิต

นี่เป็นการดำเนินการแบบเอกพจน์ ซึ่งหมายความว่าจะใช้กับชุดบิตเท่านั้น และง่ายพอๆ กับการแทนที่ 1s ด้วย 0 และในทางกลับกัน มันถูกแสดงด้วยสัญลักษณ์:~ .

~101000 = 010111

และการทำงานระดับบิต

AND เป็นการดำเนินการที่ใช้กับบิตสองชุด สัญลักษณ์ของมันคือ & และเป็นไปตามตรรกะนี้:

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

ดังนั้นเมื่อเรามีชุดบิตที่มีความยาวเท่ากันสองชุด ผลลัพธ์ก็แค่ใช้ตรรกะนี้กับแต่ละคู่:

0110 AND
0111
-----
0110

ใน Ruby:

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

หรือการทำงานระดับบิต

คล้ายกับการดำเนินการ AND การดำเนินการอื่นที่ใช้กับบิตสองชุดด้วยคือ OR ระดับบิต สัญลักษณ์ของมันคือ | และเป็นไปตามตรรกะนี้:

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

ถ้าเรามี 1 ด้านใดด้านหนึ่ง ผลลัพธ์จะเป็น 1 ไม่เช่นนั้นจะเป็น 0 ง่ายมาก! มาดูการดำเนินการ OR กับบิตเพิ่มเติม:

0110 OR
0111
-----
0111

และในทับทิม:

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

ตัวอย่างที่ใช้งานได้จริง 1:การอนุญาต

ณ จุดนี้ คุณอาจสงสัยว่าเหตุใดการดำเนินการเหล่านี้จึงมีประโยชน์ ท้ายที่สุดพวกเขาดูค่อนข้างต่ำ คุณอาจไม่มีแผนที่จะทำงานโดยตรงกับโปรโตคอลเครือข่าย กราฟิกหรือการเข้ารหัส

แต่คุณอาจเคยทำงานด้วยการอนุญาตมาก่อน สิทธิ์เป็นพื้นที่หนึ่งที่การดำเนินการระดับบิตมีประโยชน์จริงๆ ลองนึกภาพคุณมีระบบการอนุญาตที่ผู้ใช้สามารถดำเนินการต่างๆ ในเอกสารได้:

  • ดู
  • แก้ไข
  • ลบ
  • เชิญผู้ใช้รายอื่น

และตอนนี้เราต้องการจำลองการกระทำเหล่านี้ในบทบาทต่างๆ:

  • ผู้ช่วย: สามารถดูและแก้ไขเอกสารได้
  • ผู้สังเกตการณ์: สามารถดูเอกสารได้เท่านั้น
  • ผู้เขียน: สามารถดำเนินการได้ทั้งหมด

เราจะสร้างแบบจำลองนี้ได้อย่างไร เราจะรู้ได้อย่างไรว่าผู้ใช้มีบทบาทเหล่านี้อย่างใดอย่างหนึ่งเมื่อใดก็ตาม คำตอบหนึ่งคือการใช้การดำเนินการระดับบิต

เราจะเก็บบิตเพียงชุดเดียวสำหรับผู้ใช้แต่ละรายที่แสดงถึงการอนุญาตที่พวกเขามี:

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

ตัวอย่างเช่น:

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.

เมื่อเราตั้งค่าเหล่านี้สำหรับผู้ใช้บางคนแล้ว เราสามารถเปรียบเทียบอย่างรวดเร็วกับการอนุญาตที่เราต้องการได้ ลองนึกภาพว่าเรากำลังตรวจสอบว่าผู้ใช้สามารถแก้ไขเอกสารได้หรือไม่ เราสามารถใช้คำสั่ง AND ระดับบิตได้:

# 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

ซึ่งหมายความว่าหากการดำเนินการระดับบิต AND ให้สิ่งที่แตกต่างจาก 0 แก่เรา เราจะมีการตั้งค่าบิตนั้น:

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

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

เราสามารถใช้ตรรกะเดียวกันกับการอนุญาตอื่น ๆ โดยแก้ไขตำแหน่งของบิตที่เราต้องการตรวจสอบ ดังนั้นเราจะลงเอยด้วยค่าคงที่เหล่านี้และวิธีการที่เกี่ยวข้อง:

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

นอกจากนี้ เราสามารถกำหนดสิทธิ์แบบไดนามิกและจัดเก็บการอนุญาตใหม่ในอนาคตด้วยการตรวจสอบอย่างรวดเร็ว

ตัวอย่างเช่น เราได้กล่าวไว้ก่อนหน้านี้ว่าผู้ช่วยสามารถดูและแก้ไขเอกสารได้เท่านั้น ดังนั้นการอนุญาตสำหรับผู้ใช้นั้นจะเป็น:0011 . เราสามารถเก็บค่านั้นไว้ในฐานข้อมูลของเรา จากนั้นเราสามารถตรวจสอบได้อย่างง่ายดายว่าผู้ช่วยสามารถดำเนินการตามวิธีการที่กำหนดไว้ก่อนหน้านี้หรือไม่

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

หากทั้งหมดนี้ฟังดูคุ้นๆ นั่นก็เพราะเป็นวิธีเดียวกับที่ใช้กันทั่วไปสำหรับการอนุญาตไฟล์ในระบบที่ใช้ Unix

ตัวอย่างเชิงปฏิบัติ 2:ตำแหน่งในทีม

มาใช้การดำเนินการระดับบิตกันมากขึ้นหน่อย 😉

เราสามารถนำไปใช้ในกรณีอื่นที่ค่อนข้างธรรมดา:ตำแหน่งต่างๆ ในทีมกีฬาหรือตำแหน่งงานในบริษัท ไปกับทีมบาสเก็ตบอลเพื่อลดความซับซ้อน

ในบาสเก็ตบอลมี 5 ตำแหน่งในการเล่นเกม:- ตัวป้องกันจุด- ตัวป้องกันการยิง- ไปข้างหน้าเล็ก- กำลังไปข้างหน้า- ศูนย์

และเราสามารถกำหนดชุดบิตให้กับแต่ละตำแหน่งได้: 00001 Point guard 00010 Shooting guard 00100 Small forward 01000 Power forward 10000 Center

เช่นเดียวกันใน Ruby จะเป็น:

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

ตอนนี้เราทำสิ่งที่น่าสนใจได้แล้ว เช่น ตรวจสอบว่าทั้งทีมอยู่ด้วยหรือไม่:

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

ทำไม ด้วยการดำเนินการระดับบิต OR หากเรามีตำแหน่งทั้งหมด เราจะลงเอยด้วย:11111

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

และ 11111 คือ 31 เพราะ 2^0 + 2^1 + 2^2 + 2^3 + 2^4 =31

สิ่งนี้ไม่เกี่ยวข้องอย่างเคร่งครัดกับการดำเนินการระดับบิต แต่ด้วยการสร้างแบบจำลองข้อมูลนี้ เราสามารถตรวจสอบว่าสามารถแลกเปลี่ยนผู้เล่นสองคนได้หรือไม่ สิ่งนี้จะง่ายมาก:

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

XOR

การดำเนินการอื่นที่เราสามารถทำได้ด้วยบิตสองชุดคือ XOR ซึ่งมีสัญลักษณ์คือ ^ .

XOR หมายถึง OR พิเศษ และเป็นไปตามตรรกะนี้:

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

ดังนั้นผลลัพธ์จะเป็น 1 ต่อเมื่อหนึ่งในสองบิตเป็น 1; หากทั้งคู่เท่ากันจะเป็น 0

การดำเนินการนี้ใช้ในอัลกอริธึมบางตัวเพื่อเปรียบเทียบตัวเลขกับตัวมันเอง เนื่องจาก x ^ x =0

กะ

นี่คือกลุ่มการดำเนินการที่น่าสนใจที่เรา "ย้าย" บิตไปด้านใดด้านหนึ่งหรืออีกด้านหนึ่งภายในชุด ให้ฉันอธิบาย

ด้วยการเลื่อนบิต เราจะย้ายหรือ "เปลี่ยน" บิตในทิศทางเดียว ซ้ายหรือขวา:

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

เราสามารถย้ายหรือเปลี่ยนบิตได้ n ครั้ง นี่คือการเลื่อนด้านซ้ายที่ใช้กับหมายเลข 5 สองครั้งใน Ruby:

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

อย่างที่คุณเห็น การเลื่อนด้านซ้ายจะแสดงด้วย <<กะขวาใช้>>:

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

ในกรณีนี้ เราได้แค่อันเดียว เพราะบิต "0" และ "1" จาก 1*01 * ถูกทิ้ง

หารด้วย 2 ด้วยการเลื่อนขวา

ข้อเท็จจริงที่น่าสนใจประการหนึ่งเกี่ยวกับการเลื่อนบิตคือคุณสามารถคำนวณการดำเนินการทางคณิตศาสตร์บางอย่างกับมันได้ เมื่อก่อนวิธีนี้เร็วกว่า แต่ปัจจุบันนี้ใช้โดยโปรแกรมเมอร์โดยเฉพาะซึ่งทำงานกับข้อจำกัดด้านทรัพยากรมากขึ้น เช่น ในการพัฒนาเกม

หากเราใช้กะทางขวากับตัวเลข เราจะได้ผลลัพธ์จากการหารด้วย 2:

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

คูณด้วย 2 ด้วยการเลื่อนซ้าย

ในทำนองเดียวกัน เราสามารถคูณด้วย 2 ด้วยการเลื่อนซ้าย:

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

ตรวจสอบอย่างรวดเร็วว่าตัวเลขเป็นเลขคี่หรือคู่

มีตัวอย่างง่ายๆ ของการดำเนินการระดับบิตที่รวดเร็วและง่ายต่อการติดตาม

ลองนึกภาพเราดำเนินการ AND ด้วย 1 เพียง 1 นั่นก็คือจำนวน 0 เท่าใดก็ได้ ขึ้นอยู่กับคอมพิวเตอร์ที่เราอยู่ และ 1. ลองทำด้วย 2:

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

ตอนนี้มี 4:

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

แล้ว 5 ล่ะ?

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

ตอนนี้เราได้ 1 แล้วคุณเดาได้ไหมว่านี่หมายถึงอะไร

การทำเช่นนี้ AND กับ 1 หากตัวเลขเป็นเลขคู่ เราจะได้ 0 ถ้ามันแปลก เราจะได้ 1 เราสามารถใช้ข้อเท็จจริงนี้เพื่อสร้างวิธีการสองสามวิธีใน 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

หากคุณต้องการไปต่อหรือสัมผัสช่วงเวลาที่น่าประทับใจด้วยบิต ให้ลองดูคอลเล็กชันการแฮ็กระดับบิตเพื่อดูกลเม็ดเพิ่มเติม

บทสรุป

การดำเนินงานระดับ Bitwise นั้นยากต่อการติดตามหากคุณไม่เคยพบเจอมาก่อน แต่เมื่อคุณคุ้นเคยกับมันแล้ว คุณจะสามารถเตรียมรับมือกับโครงการที่มีอยู่หรือในอนาคตได้ดียิ่งขึ้น นอกจากนี้ คุณจะมีเครื่องมือใหม่ในการวางแผนวิธีแก้ปัญหาในโค้ดของคุณ