Rust में defensive programming patterns
(corrode.dev)- Rust के type system और compiler का सक्रिय उपयोग करके bugs को पहले से रोकने वाली coding आदतों का परिचय
- vector indexing,
Defaultका अति-उपयोग, अपूर्णmatch, और अनावश्यक boolean parameters जैसे कमज़ोर code smell के उदाहरण और उनके विकल्पों की व्याख्या - मुख्य सिद्धांत है ऐसी संरचना डिज़ाइन करना जिसमें compiler invariants को enforce करे, और इसके लिए pattern matching, private fields,
#[must_use]attribute आदि का उपयोग TryFromका उपयोग, struct का complete destructuring, temporary mutability, constructor validation जैसे वास्तविक code-level defensive techniques को ठोस रूप में प्रस्तुत किया गया है- ऐसे patterns refactoring के दौरान stability सुनिश्चित करने और long-term maintainability बढ़ाने के लिए आवश्यक हैं
defensive programming का अवलोकन
// this should never happencomment वाले स्थान उन बिंदुओं को दिखाते हैं जहाँ implicit invariant टूटता है- अधिकांश मामलों में developer सभी boundary conditions या भविष्य के code changes को ध्यान में नहीं रखता
- Rust compiler memory safety की गारंटी देता है, लेकिन business logic errors फिर भी हो सकते हैं
- कई वर्षों के व्यावहारिक अनुभव से मिले छोटे habitual patterns (idiom) code quality को बहुत बेहतर बनाते हैं
Code Smell: vector indexing
if !vec.is_empty() { let x = &vec[0]; }जैसी संरचना में length check और indexing अलग होने से runtime panic का जोखिम रहता है- slice pattern matching (
match vec.as_slice()) का उपयोग करने पर compiler हर state की जाँच अनिवार्य कर देता है- empty vector, single element, duplicate element जैसे सभी मामलों को स्पष्ट रूप से handle किया जा सकता है
- यह ऐसा डिज़ाइन करने का प्रतिनिधि उदाहरण है जहाँ compiler invariants की गारंटी दे
Code Smell: Default का अंधाधुंध उपयोग
..Default::default()नया field जुड़ने पर उसके छूट जाने के जोखिम और implicit value setting की समस्या पैदा कर सकता है- सभी fields को स्पष्ट रूप से initialize करने पर compiler नए fields को सेट करने के लिए मजबूर करता है
let Foo { field1, field2, .. } = Foo::default();जैसे रूप में default struct को destructure करके चुनिंदा override किया जा सकता है- इससे default values और explicit override के बीच संतुलन बना रहता है
Code Smell: नाज़ुक Trait implementation
- struct fields को पूरी तरह destructure करके compare करने पर नया field जुड़ने पर compiler error के ज़रिए चेतावनी मिलती है
- उदाहरण:
PartialEqimplementation मेंlet Self { size, toppings, .. } = self;
- उदाहरण:
extra_cheeseजैसे नए field जुड़ने पर comparison logic की फिर से समीक्षा करना अनिवार्य हो जाता है- यही सिद्धांत
Hash,Debug,Cloneजैसे अन्य traits पर भी लागू किया जा सकता है
Code Smell: From की जगह TryFrom की आवश्यकता
- जब conversion हमेशा सफल नहीं होता, तब
Fromकी जगहTryFromसे failure की संभावना को स्पष्ट करना चाहिए unwrap_or_elseका उपयोग संभावित failure को छिपाने का संकेत हो सकता है, और fail fast तरीका अधिक सुरक्षित है
Code Smell: अपूर्ण match
_ => {}जैसा catch-all pattern नया variant जुड़ने पर case छूट जाने का जोखिम बढ़ाता है- सभी variants को स्पष्ट रूप से सूचीबद्ध करने पर compiler नए case को handle न करने की चेतावनी देता है
- वही logic
Variant3 | Variant4जैसे रूप में group भी किया जा सकता है
Code Smell: _ placeholder का अति-उपयोग
- केवल
_इस्तेमाल करने पर कौन-सा variable छोड़ा गया है यह स्पष्ट नहीं रहता has_fuel: _, has_crew: _जैसे स्पष्ट नाम readability बेहतर बनाते हैं
Pattern: temporary mutability
- जब data सिर्फ initialization के दौरान mutable होना चाहिए, तब
let mut data = ...; data.sort(); let data = data;जैसी संरचना उपयोगी है - block scope का उपयोग करने पर temporary variables बाहर expose होने से बचते हैं
- उदाहरण:
let data = { let mut d = get_vec(); d.sort(); d };
- उदाहरण:
- कई temporary variables वाले initialization process में scope को स्पष्ट रूप से अलग किया जा सकता है
Pattern: constructor validation को अनिवार्य बनाना
- struct बनाते समय validation logic से अनिवार्य रूप से गुज़रना सुनिश्चित किया जाता है
_private: ()field जोड़ने पर बाहर से direct construction संभव नहीं रहता#[non_exhaustive]attribute crate के बाहर construction रोकने और future extension का संकेत देने के लिए उपयोगी है
- अगर internal modules में भी इसे enforce करना हो, तो private type (
Seal) वाली nested module structure का उपयोग किया जा सकता हैSealकेवल अंदर मौजूद होता है, इसलिएnew()के अलावा direct construction संभव नहीं
- fields को private रखकर getter देने से immutable state बनाए रखी जा सकती है
- लागू करने के मानदंड
- बाहरी code को रोकना:
_privateया#[non_exhaustive] - आंतरिक code को रोकना: private module +
Seal - validation logic को compiler-level guarantee में बदलना
- बाहरी code को रोकना:
Pattern: #[must_use] attribute का उपयोग
#[must_use]महत्वपूर्ण return values को नज़रअंदाज़ होने से रोकता है- उदाहरण:
#[must_use = "Configuration must be applied to take effect"]
- उदाहरण:
- यदि user return value को ignore करता है, तो compiler warning देता है
- यह
Resultजैसी standard library types में भी व्यापक रूप से इस्तेमाल होने वाला सरल लेकिन शक्तिशाली defensive उपाय है
Code Smell: boolean parameters
fn process_data(..., compress: bool, encrypt: bool, validate: bool)जैसी संरचना अर्थ को अस्पष्ट बनाती है और order mistakes का जोखिम बढ़ाती हैenum Compression,enum Encryptionआदि से intent को स्पष्ट रूप से व्यक्त किया जा सकता है- कई options होने पर parameter struct (Params struct) का उपयोग उपयुक्त है
ProcessDataParams::production()जैसी preset methods reusability बढ़ाती हैं
- नया option जुड़ने पर मौजूदा call sites पर असर कम होता है
Clippy lints के साथ automation
- प्रमुख defensive patterns की Clippy lints से automatic जाँच की जा सकती है
indexing_slicing: direct indexing निषिद्धfallible_impl_from:Fromकी जगहTryFromकी सिफारिशwildcard_enum_match_arm:_pattern निषिद्धfn_params_excessive_bools: बहुत अधिक boolean parameters पर warningmust_use_candidate:#[must_use]candidates का सुझाव
#![deny(clippy::...)]याCargo.tomlsettings से इन्हें project-wide लागू किया जा सकता है
निष्कर्ष
- Rust में defensive programming का सार है type system और compiler का सक्रिय उपयोग करके invariants को explicit और verifiable बनाना
- ये patterns refactoring के समय stability, bug की संभावना में कमी, और long-term maintainability में योगदान देते हैं
- यह “जो bug compile ही न हो, वही सबसे अच्छा bug है” वाले सिद्धांत को व्यवहार में उतारने का तरीका है
1 टिप्पणियां
Hacker News राय
लेख अच्छा था। लेकिन PizzaOrder उदाहरण में ऐसा लगा कि बहुत सारी अलग-अलग concerns एक ही struct में ठूँस दी गई हैं
अगर मकसद
ordered_atको comparison से बाहर रखना है, तोPizzaDetailsऔरPizzaOrderदो struct में बाँटना बेहतर होगाइससे
PartialEqimplement करते समय सिर्फdetailsको compare करना साफ़ तौर पर व्यक्त किया जा सकता हैअगर order का समय अलग है, तो वह वही order नहीं है, इसलिए type level पर उन्हें equal define करना जोखिम भरा है
PizzaDetailsपरPartialEqरखना ठीक है, लेकिन order comparison logic को अलग business function में रखना सही होगाPizzaDetailsमें बदलाव होने पर उसका असर pizza deduplication logic पर पड़ सकता हैआदर्श रूप से struct का इस्तेमाल सिर्फ data को समूहित करने के लिए होना चाहिए
ताकि बदलाव दूसरी जगह असर न डालें,
PizzaComparatorयाPizzaFlavorजैसे अलग type रखने का तरीका भी सोचा जा सकता हैProtobuf की तरह fields पर
{important_to_flavour=true}जैसी field annotation हो सके तो अच्छा होगाउदाहरण के लिए अगर string को case-insensitive compare करना हो, तो उसे कैसे अलग करेंगे?
Rust की सचमुच शानदार बात यह है कि कई बार defensive programming की ज़रूरत ही नहीं पड़ती
ownership और reference rules की वजह से यह गारंटी मिल सकती है कि किसी खास object तक पहुँच पूरे program में unique है
reference null नहीं हो सकता, और smart pointer भी null नहीं हो सकता
type system यह भी सुनिश्चित करता है कि
selfकी ownership दे देने के बाद उस पर method call नहीं हो सकतीइसकी वजह से thread safety, lifetime, clonability जैसी चीज़ें compile time पर global level पर verify हो जाती हैं
दूसरी भाषाओं में functional style अपनाकर immutability बनाए रखने से जो फायदे मिलते हैं, Rust उन्हें type system से enforce करता है
लेख का विषय ऐसे logical bugs थे जिन्हें borrow checker भी नहीं पकड़ता
array या vector में सीधे indexing करने से बचना समझदारी लगता है
जिस दिन Cloudflare का unwrap incident हुआ था, उसी दिन मुझे भी slice के vector के अंत से आगे निकल जाने वाला bug मिला था
उसके बाद मैंने iterator-based approach अपनाई और वह कहीं ज़्यादा सुरक्षित लगी
Rust का
unwrapC केassertजैसा है। fail होने पर उसका काम सिर्फ समस्या बताना हैRust में भी bug अब भी लिखे जा सकते हैं
Rust developers को जिन आदतों से बचना चाहिए, उनमें एक है अनावश्यक crate dependency जोड़ना
Rust में ऐसी आदत को बढ़ावा देने की प्रवृत्ति है। उदाहरण के लिए Rust Book में
randcrate को basic example के रूप में इस्तेमाल करना भी ऐसा माहौल बनाता हैबेशक यह crypto-related packages को आसानी से बदल पाने के लिए किया गया एक रणनीतिक चुनाव है, लेकिन फिर भी इसका आदत बन जाना समस्या है
लेकिन बाद में उसका इरादा समझ में आया और मेरी राय बदल गई
partial equality implementation दिलचस्प लगी
एक और बात जो जाननी है, वह है boolean parameter से बचते समय enum का इस्तेमाल कैसे किया जाए
मैं bool को wrap करने वाला struct इस्तेमाल करता हूँ, लेकिन अफसोस यह है कि उसे सामान्य bool की तरह handle नहीं किया जा सकता
सोचता हूँ कि क्या enum को bool जैसा इस्तेमाल करने का कोई तरीका है
ज़रूरी logic को Trait में बाँधकर, या
impl <Enum>block में common methods जोड़कर इसे संभालता हूँइससे readability भी अच्छी रहती है और हर member का behavior साफ़ तौर पर define किया जा सकता है
impl Derefजैसी चीज़ आज़माई जा सकती है, लेकिन यह अच्छा idea है या नहीं, इस पर यक़ीन नहीं हैपहले उदाहरण का
matchstatement कुछ ज़्यादा ही भारी लगता हैVec.first()याVec.iter().nth(0)ज़्यादा साफ़ और इरादे के मुताबिक हैंmatchका इस्तेमाल करने पर उल्टा समस्या से ज़्यादा जटिल समाधान बन जाता हैअगर
ifहटाया जा सकता है तोmatchभी हटाया जा सकता है, इसलिए safety के नज़रिए से कोई फ़र्क नहीं पड़ताfirst()कहीं ज़्यादा संक्षिप्त और स्पष्ट हैmatchइस मायने में उपयोगी है कि वह “जब element एक या अधिक हों” वाले case को भी handle करने के लिए प्रेरित करता हैयानी यह check और उस पर निर्भर code को अलग न करो वाले सिद्धांत को सामने लाता है
हर बार ऐसे लेख पढ़कर सोचता हूँ कि code patterns को monitor करने वाली dedicated team क्यों नहीं होती
SOC या QA की तरह अगर कोई team codebase के patterns को लंबे समय तक देखती रहे, तो अच्छा होगा
automated code smell detection tools की अपनी सीमाएँ हैं
वह lint rules management, documentation, developer training, और shared libraries maintenance संभालती है
जब कई teams बार-बार एक जैसी समस्या झेलती हैं, तो वह उन्हें समेटने वाला core API design करती है
लेकिन जब code लाखों lines का हो, तो उसे manage करना बहुत मुश्किल हो जाता है
सोच रहा हूँ कि team के भीतर ऐसे अच्छे coding patterns को कैसे बढ़ावा दिया जाए
code review के दौरान यह अक्सर “style debate” बनकर गैर-उत्पादक हो जाता है
लेकिन अजीब बात यह है कि linter warning दे दे, तो ऐसी बहसें लगभग गायब हो जाती हैं
TryFromtrait का version 1.34 में जुड़ना सचमुच बहुत उपयोगी थाशायद
unwrap_or_else()इस्तेमाल करने वाला code उससे पहले के समय का अवशेष हो सकता हैFrom trait documentation अब यह बहुत साफ़ बताती है कि इसे कब implement करना चाहिए
unwrap_or_else()नाम सुनकर मज़ेदार लगता है, जैसे “computer को धमकाते हुए आदेश दे रहे हों”मुझे लगता है कि ऐसे defensive programming patterns बड़े पैमाने पर AI code generation की quality सुधारने में भी मददगार होंगे
Clippy और Rust compiler से मिलने वाला ठोस feedback, AI agents को गलतियाँ कम करने और सही दिशा पकड़ने में बड़ी भूमिका निभा सकता है