การส่งอาร์เรย์และแผนที่เชื่อมโยงระหว่างฟังก์ชัน
ใช้ nameref และการขยายค่าแบบอ้อม เพื่อส่งโครงสร้างข้อมูลซับซ้อนเข้าและออกจากฟังก์ชันอย่างปลอดภัย
การส่งอาร์เรย์และแผนที่เชื่อมโยงระหว่างฟังก์ชัน เป็นบทเรียน Linux Command Line & Bash Scripting Mastery ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Linux Command Line & Bash Scripting Mastery และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Linux Command Line & Bash Scripting Mastery มีบทเรียนทั้งหมด 4 บทเรียน
เหตุใดการส่งอาร์เรย์ให้ฟังก์ชันจึงทำได้ยุ่งยาก
ใน Bash อาร์เรย์และอาร์เรย์เชื่อมโยง ไม่ใช่ค่าแบบชั้นหนึ่ง เมื่อคุณพยายามส่งอาร์เรย์ให้ฟังก์ชัน คุณไม่สามารถใช้ $my_array ได้โดยตรง เพราะวิธีนี้จะขยายค่าออกมาเพียงองค์ประกอบแรกเท่านั้น
ลองพิจารณาความพยายามที่ใช้ไม่ได้ต่อไปนี้:
my_func "${my_array[@]}"ฟังก์ชันจะได้รับรายการคำแบบแบน จึงไม่สามารถทราบได้ว่าองค์ประกอบหนึ่งสิ้นสุดตรงไหนและองค์ประกอบถัดไปเริ่มตรงไหน และโครงสร้างอาร์เรย์จะหายไปทั้งหมด
Bash มีกลไกที่เชื่อถือได้สองวิธีสำหรับส่งโครงสร้างข้อมูลที่ซับซ้อน:
- การอ้างอิงด้วยชื่อ (
declare -n) — การอ้างอิงตัวแปรอื่นด้วยชื่อ (Bash 4.3 ขึ้นไป) - การขยายค่าโดยอ้อม (
${!varname[@]}) — เข้าถึงตัวแปรที่มีชื่อเก็บอยู่ในตัวแปรอื่น (สำหรับ Bash รุ่นเก่า)
บทเรียนนี้ครอบคลุมทั้งสองเทคนิค เพื่อให้คุณเขียนฟังก์ชันที่ทนทานและนำกลับมาใช้ซ้ำได้ ซึ่งทำงานกับอาร์เรย์และแมป
การส่งอาร์เรย์ด้วยชื่อโดยใช้การอ้างอิงด้วยชื่อ
แนวทางสมัยใหม่ที่สะอาดที่สุดคือ การอ้างอิงด้วยชื่อ: คุณส่ง ชื่อ ของอาร์เรย์เป็นสตริง แล้วภายในฟังก์ชันประกาศตัวแปรเฉพาะที่ซึ่งอ้างอิงไปยังอาร์เรย์นั้น
ไวยากรณ์สำคัญ:
declare -n ref="$1"— ขณะนี้refเป็นชื่อแทนของตัวแปรใดก็ตามที่ส่งมาเป็นอาร์กิวเมนต์แรก- คุณอ่าน วนซ้ำ และแก้ไข
refได้เช่นเดียวกับอาร์เรย์ต้นฉบับทุกประการ
วิธีนี้หลีกเลี่ยงการคัดลอกอาร์เรย์ทั้งหมด และทำให้ฟังก์ชันทำงานกับอาร์เรย์ที่มีขนาดใดก็ได้
#!/usr/bin/env bash
print_array() {
declare -n arr="$1" # nameref: arr -> caller's variable
echo "Array has ${#arr[@]} elements:"
for elem in "${arr[@]}"; do
echo " - $elem"
done
}
fruits=(apple banana cherry)
print_array fruitsการแก้ไขอาร์เรย์ของผู้เรียกภายในฟังก์ชัน
เนื่องจากการอ้างอิงด้วยชื่อเป็นชื่อแท้จริง การ เขียนค่าผ่านการอ้างอิงด้วยชื่อจะเปลี่ยนตัวแปรต้นฉบับของผู้เรียก จึงไม่จำเป็นต้องส่งค่ากลับด้วยการแสดงค่า หรือใช้ตัวแปรส่วนกลาง
ด้วยเหตุนี้ การอ้างอิงด้วยชื่อจึงเหมาะอย่างยิ่งกับฟังก์ชันที่แปลงอาร์เรย์โดยแก้ไขอาร์เรย์เดิมโดยตรง เช่น การปรับรูปแบบสตริง การกรององค์ประกอบ หรือการเรียงลำดับ
#!/usr/bin/env bash
to_uppercase() {
declare -n _arr="$1"
for i in "${!_arr[@]}"; do
_arr[$i]="${_arr[$i]^^}" # ^^ = uppercase in Bash 4+
done
}
words=(hello world bash)
echo "Before: ${words[@]}"
to_uppercase words
echo "After: ${words[@]}"การคืนอาร์เรย์ใหม่ผ่านการอ้างอิงด้วยชื่อ
ฟังก์ชันยังสามารถ เขียนลงในอาร์เรย์ที่ผู้เรียกจัดเตรียมไว้ ได้ โดยรับชื่ออาร์เรย์เป็นพารามิเตอร์ข้อมูลออก ผู้เรียกสร้างอาร์เรย์ว่าง ส่งชื่อของอาร์เรย์นั้นไป แล้วฟังก์ชันจะเติมข้อมูลลงไป
รูปแบบนี้คล้ายสำนวนการเขียนโปรแกรมแบบดั้งเดิมในภาษา C ที่ส่งตัวชี้ไปยังบัฟเฟอร์ผลลัพธ์ แต่ในกรณีนี้ทำทั้งหมดภายในเชลล์
#!/usr/bin/env bash
get_even_numbers() {
local -a source=("$@")
# Last argument is the output array name — pop it off
local out_name="${source[-1]}"
unset 'source[-1]'
declare -n _out="$out_name"
_out=() # clear
for n in "${source[@]}"; do
(( n % 2 == 0 )) && _out+=("$n")
done
}
declare -a evens
get_even_numbers 1 2 3 4 5 6 7 8 evens
echo "Evens: ${evens[@]}"กับดักการชนกันของการอ้างอิงด้วยชื่อ
การอ้างอิงด้วยชื่อมีข้อผิดพลาดสำคัญอยู่ประการหนึ่ง: หาก ตัวแปรการอ้างอิงด้วยชื่อเฉพาะที่มีชื่อเดียวกับตัวแปรของผู้เรียก Bash จะเข้าสู่วงวนการอ้างอิงตัวเองไม่รู้จบ และสคริปต์จะทำงานล้มเหลว
ตัวอย่างเช่น หากผู้เรียกตั้งชื่ออาร์เรย์ว่า arr และฟังก์ชันใช้คำสั่ง declare -n arr="$1" การอ้างอิงด้วยชื่อ arr ก็จะชี้กลับมาที่ตัวเอง
หลักปฏิบัติเพื่อหลีกเลี่ยงปัญหานี้: เติมขีดล่างหน้าชื่อตัวแปรการอ้างอิงด้วยชื่อภายใน เช่น _arr, _ref, _out ผู้เรียกแทบไม่ใช้ชื่อในรูปแบบนี้ จึงทำให้การชนกันเกิดขึ้นได้ยากมาก
#!/usr/bin/env bash
# DANGER: caller uses variable named 'arr'
arr=(one two three)
bad_func() {
declare -n arr="$1" # collision! arr refers to itself
echo "${arr[@]}"
}
good_func() {
declare -n _arr="$1" # underscore prefix avoids collision
echo "${_arr[@]}"
}
# bad_func arr # would hang / error
good_func arrการส่งอาร์เรย์เชื่อมโยง (แมป) ด้วยชื่อ
อาร์เรย์เชื่อมโยง (declare -A) ทำงานกับการอ้างอิงด้วยชื่อได้ในลักษณะเดียวกัน ส่งชื่อของแมปเป็นสตริง แล้วประกาศการอ้างอิงด้วยชื่อภายในฟังก์ชัน คุณจะเข้าถึงเพื่ออ่านและเขียนคู่คีย์-ค่าได้ทั้งหมด
สิ่งนี้มีประโยชน์อย่างยิ่งสำหรับแมปการกำหนดค่า ตัวแยกวิเคราะห์ตัวเลือก และตารางค้นหาที่ต้องใช้ร่วมกันระหว่างฟังก์ชันในไลบรารี
#!/usr/bin/env bash
print_map() {
declare -n _map="$1"
echo "Map contents:"
for key in "${!_map[@]}"; do
printf ' %-15s => %s\n' "$key" "${_map[$key]}"
done
}
declare -A config
config[host]="localhost"
config[port]="5432"
config[db]="myapp"
print_map configการรวมแมปสองรายการด้วยการอ้างอิงด้วยชื่อ
เนื่องจากการอ้างอิงด้วยชื่อทำให้ฟังก์ชันมีชื่อแทนที่เชื่อมโยงกับข้อมูลจริงอยู่เสมอ คุณจึงสร้างเครื่องมืออย่าง การรวมแมป ได้อย่างเป็นระเบียบ ส่งแมปปลายทางและแมปต้นทางหนึ่งรายการขึ้นไปด้วยชื่อ จากนั้นฟังก์ชันจะวนอ่านคีย์จากต้นทางและเขียนคีย์เหล่านั้นลงในปลายทาง
การอ้างอิงด้วยชื่อทำให้สามารถเขียนฟังก์ชันทั่วไป merge_map เพียงฟังก์ชันเดียว ซึ่งทำงานกับอาร์เรย์เชื่อมโยงใด ๆ ในสคริปต์ของคุณได้
#!/usr/bin/env bash
merge_map() {
# merge_map dest src
declare -n _dest="$1"
declare -n _src="$2"
for key in "${!_src[@]}"; do
_dest["$key"]="${_src[$key]}"
done
}
declare -A defaults=([timeout]=30 [retries]=3 [verbose]=false)
declare -A overrides=([retries]=5 [log_level]=debug)
merge_map defaults overrides
for k in "${!defaults[@]}"; do
echo "$k = ${defaults[$k]}"
doneการขยายค่าโดยอ้อม: ความเข้ากันได้กับ Bash รุ่นเก่า
หากคุณต้องรองรับ Bash 4.0–4.2 (หรือเชลล์เริ่มต้นรุ่นเก่าของ macOS) จะไม่สามารถใช้ declare -n ได้ ทางเลือกคือ การขยายค่าโดยอ้อมด้วย ${!varname}
การขยายค่าโดยอ้อมจะประเมินค่าของ varname ให้เป็นชื่อตัวแปร แล้วคืนค่าของตัวแปรนั้น สำหรับอาร์เรย์ คุณต้องใช้ร่วมกับไวยากรณ์พิเศษเพื่อรับองค์ประกอบทั้งหมด
ข้อจำกัดเมื่อเทียบกับการอ้างอิงด้วยชื่อ:
- ในทางปฏิบัติอ่านได้อย่างเดียว (การเขียนค่ากลับต้องใช้
evalซึ่งมีความเสี่ยงจากการแทรกคำสั่ง) - ไม่สามารถวนอ่านคีย์ของอาร์เรย์เชื่อมโยงได้อย่างปลอดภัยหากไม่ใช้
eval - ใช้การอ้างอิงด้วยชื่อเมื่อทำได้ และใช้การอ้างอิงโดยอ้อมเป็นทางเลือกสำรองเฉพาะเมื่อจำเป็น
#!/usr/bin/env bash
sum_array() {
local arr_name="$1"
# Indirect: build a temporary copy by expanding arr_name[@]
local -a items
eval "items=( \"\${${arr_name}[@]}\" )"
local total=0
for n in "${items[@]}"; do
(( total += n ))
done
echo "$total"
}
numbers=(10 20 30 40)
result=$(sum_array numbers)
echo "Sum: $result"การสร้างไลบรารีสคริปต์แบบแยกส่วน
ในโครงการ Bash ระดับมืออาชีพ ฟังก์ชันอรรถประโยชน์จะจัดเก็บไว้ใน ไฟล์ไลบรารี เช่น lib/array_utils.sh และถูกนำเข้ามาโดยสคริปต์ที่ต้องใช้งาน
รูปแบบมีดังนี้:
- ไฟล์ไลบรารีแต่ละไฟล์กำหนดเฉพาะฟังก์ชันเท่านั้น ไม่มีผลข้างเคียงระดับบนสุด
- ป้องกันการนำเข้าไฟล์ซ้ำด้วยตัวแปรบอกสถานะ:
[[ -n $__LIB_LOADED ]] && return; __LIB_LOADED=1 - สคริปต์ผู้ใช้ใช้
sourceหรือ.เพื่อโหลดไลบรารี - ฟังก์ชันแลกเปลี่ยนข้อมูลที่ซับซ้อนผ่านการอ้างอิงด้วยชื่อ ทำให้ตรรกะทั้งหมดอยู่ภายในตัวเอง
วิธีนี้ทำให้ได้สคริปต์ที่ทดสอบได้ นำกลับมาใช้ซ้ำได้ และดูแลรักษาได้ ไม่ใช่การแก้ปัญหาเชลล์เฉพาะกิจ
ตัวอย่างเชิงปฏิบัติ: ตัวแยกวิเคราะห์ตัวเลือกที่คืนค่าเป็นแมป
กรณีใช้งานจริง: ตัวแยกวิเคราะห์ตัวเลือกบรรทัดคำสั่งที่เติมข้อมูลลงในอาร์เรย์เชื่อมโยงที่ผู้เรียกจัดเตรียมไว้ ผู้เรียกส่งชื่ออาร์เรย์เป็นอาร์กิวเมนต์แรก ตามด้วยอาร์กิวเมนต์ "$@" จริง
รูปแบบนี้ทำให้สคริปต์ใด ๆ นำตัวแยกวิเคราะห์เดียวกันกลับมาใช้ได้ โดยไม่ต้องคัดลอกโค้ดหรือพึ่งพาตัวแปรส่วนกลาง
#!/usr/bin/env bash
parse_opts() {
declare -n _opts="$1"
shift
while [[ $# -gt 0 ]]; do
case "$1" in
--host) _opts[host]="$2"; shift 2 ;;
--port) _opts[port]="$2"; shift 2 ;;
--user) _opts[user]="$2"; shift 2 ;;
*) echo "Unknown option: $1" >&2; shift ;;
esac
done
}
declare -A opts=([host]=localhost [port]=5432 [user]=admin)
parse_opts opts --host db.example.com --port 3306
echo "Connecting to ${opts[host]}:${opts[port]} as ${opts[user]}"การรวมการอ้างอิงด้วยชื่อของข้อมูลเข้าและข้อมูลออกในฟังก์ชันเดียว
ฟังก์ชันสามารถมี การอ้างอิงด้วยชื่อหลายรายการพร้อมกัน ได้ โดยรายการหนึ่งใช้สำหรับข้อมูลเข้า และอีกรายการใช้สำหรับข้อมูลออก วิธีนี้ทำให้สร้างฟังก์ชันแปลงข้อมูลบริสุทธิ์ได้: ฟังก์ชันอ่านโครงสร้างต้นทาง คำนวณผลลัพธ์ แล้วเขียนลงในโครงสร้างปลายทาง โดยไม่แตะต้องสถานะส่วนกลาง
การใช้ชื่อที่แตกต่างกันและเติมขีดล่างหน้าชื่อของการอ้างอิงด้วยชื่อแต่ละรายการ ทำให้ไม่เกิดการชนกัน แม้ตัวแปรของผู้เรียกจะมีชื่อสั้น ๆ ก็ตาม
#!/usr/bin/env bash
filter_map_by_key_prefix() {
# Usage: filter_map_by_key_prefix src_map dest_map prefix
declare -n _src="$1"
declare -n _dst="$2"
local prefix="$3"
_dst=()
for key in "${!_src[@]}"; do
if [[ $key == ${prefix}* ]]; then
_dst["$key"]="${_src[$key]}"
fi
done
}
declare -A settings=(
[db_host]=localhost [db_port]=5432
[cache_ttl]=300 [cache_driver]=redis
[app_debug]=true
)
declare -A db_settings
filter_map_by_key_prefix settings db_settings db_
for k in "${!db_settings[@]}"; do
echo "$k = ${db_settings[$k]}"
doneตรวจสอบความเข้าใจ: การอ้างอิงด้วยชื่อและการส่งอาร์เรย์
ทดสอบความเข้าใจของคุณเกี่ยวกับการส่งอาร์เรย์และแมประหว่างฟังก์ชันใน Bash
ทบทวน: การส่งโครงสร้างข้อมูลที่ซับซ้อนใน Bash
สิ่งที่คุณได้เรียนรู้ในบทเรียนนี้มีดังนี้:
- ไม่สามารถส่งอาร์เรย์แบบส่งค่าได้ — ให้ส่ง ชื่อ ของตัวแปรเป็นสตริงแทน
declare -n ref="$varname"สร้างการอ้างอิงด้วยชื่อ ซึ่งเป็นชื่อแทนที่เชื่อมโยงกับตัวแปรของผู้เรียกอยู่เสมอ ทั้งการอ่านและการเขียนจะส่งต่อไปยังต้นฉบับ ต้องใช้ Bash 4.3 ขึ้นไป- การชนกันของการอ้างอิงด้วยชื่อ เกิดขึ้นเมื่อตัวแปรภายในและภายนอกมีชื่อเดียวกัน ป้องกันได้โดยเติมขีดล่างหน้าชื่อการอ้างอิงด้วยชื่อภายใน เช่น
_arr,_map - พารามิเตอร์ข้อมูลออกทำงานโดยส่งชื่อของอาร์เรย์หรือแมปผลลัพธ์ที่ว่างเปล่า แล้วให้ฟังก์ชันเติมข้อมูลผ่านการอ้างอิงด้วยชื่อ
- การอ้างอิงด้วยชื่อหลายรายการในฟังก์ชันเดียว ทำให้แปลงข้อมูลจากข้อมูลเข้าเป็นข้อมูลออกได้โดยไม่มีผลข้างเคียงต่อสถานะส่วนกลาง
- การขยายค่าโดยอ้อม (
${!varname[@]}+eval) เป็นทางเลือกสำรองแบบเก่าสำหรับ Bash < 4.3 แต่มีความเสี่ยงจากการแทรกคำสั่ง และในทางปฏิบัติอ่านได้อย่างเดียว - ผสานเทคนิคเหล่านี้เข้ากับ ไฟล์ไลบรารีที่นำเข้า เพื่อสร้างโค้ด Bash ที่เป็นแบบแยกส่วน นำกลับมาใช้ซ้ำได้ และทดสอบได้
คำถามที่พบบ่อย
บทเรียน “การส่งอาร์เรย์และแผนที่เชื่อมโยงระหว่างฟังก์ชัน” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “การส่งอาร์เรย์และแผนที่เชื่อมโยงระหว่างฟังก์ชัน” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Linux Command Line & Bash Scripting Mastery ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Linux Command Line & Bash Scripting Mastery มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การส่งอาร์เรย์และแผนที่เชื่อมโยงระหว่างฟังก์ชัน”
ใช้ nameref และการขยายค่าแบบอ้อม เพื่อส่งโครงสร้างข้อมูลซับซ้อนเข้าและออกจากฟังก์ชันอย่างปลอดภัย คุณปฏิบัติ Linux Command Line & Bash Scripting Mastery ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Linux Command Line & Bash Scripting Mastery หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Linux Command Line & Bash Scripting Mastery บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน
บทเรียน “การส่งอาร์เรย์และแผนที่เชื่อมโยงระหว่างฟังก์ชัน” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Linux Command Line & Bash Scripting Mastery นี้ได้ไหม
ได้ บทเรียน Linux Command Line & Bash Scripting Mastery ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- การออกแบบฟังก์ชันด้วยขอบเขตภายในและรหัสส่งคืน
- การสร้างและโหลดไลบรารี Bash ที่นำกลับมาใช้ซ้ำได้
- การแยกแฟล็กและอาร์กิวเมนต์ด้วย getopts
- การส่งอาร์เรย์และแผนที่เชื่อมโยงระหว่างฟังก์ชัน