Fermer

décembre 10, 2025

Opérateur d’affectation conditionnelle nulle C# 14

Opérateur d’affectation conditionnelle nulle C# 14


Cet article présente l’opérateur d’affectation conditionnelle nulle, une nouvelle fonctionnalité de C# 14 qui vous permet d’écrire du code propre et concis.

Quand on regarde le Mises à jour C# 14 à venirj’ai découvert une fonctionnalité et j’ai pensé : « Comment cela n’existait-il pas déjà ? Si vous utilisez les opérateurs conditionnels nuls de C# (?. et ?[]) depuis des années comme moi, vous allez adorer le support pour affectations conditionnelles nulles.

Notre long voyage nul

Il a fallu un long chemin pour améliorer la façon dont les développeurs C# travaillent avec null, l’erreur d’un milliard de dollars.

C# 2 a lancé les choses avec des types de valeurs nullables (comme int? et bool?) car parfois vous avez besoin de savoir si quelqu’un n’a pas saisi un nombre plutôt que de saisir zéro.

Avec C# 6, nous avons obtenu des opérateurs conditionnels nuls (?. et ?[]), nous permettant d’enchaîner des objets potentiellement nuls sans écrire de romans. C# 7 nous a donné une correspondance de modèles avec is null des chèques qui se lisent comme de l’anglais.

C# 8 est sorti avec des types de référence nullables, l’opérateur de fusion nulle (??=), et l’opérateur indulgent nul (!) lorsque vous pensez savoir mieux que le compilateur. Et bien sûr, C# 9 l’a complété avec is not null parce que is null je me sentais seul.

Pour moi, les opérateurs conditionnels nuls de C# 6 ont changé la donne. Au lieu de vérifier chaque niveau de valeurs nulles potentielles, je peux simplement l’enchaîner.


string? city = null;
if (customer != null && customer.Address != null)
{
    city = customer.Address.City;
}
    

string? city = customer?.Address?.City;

C’était idéal pour lire les valeurs. Cependant, nous ne pourrons jamais utiliser la même astuce pour en écrivant valeurs. Entrez C#14.

Le problème qu’il résout

Combien de fois avez-vous écrit du code comme celui-ci ?

if (customer != null)
  customer.Order = GetOrder(customer.Id)

Si vous êtes comme moi, ce modèle est gravé dans votre mémoire. Je le tape sans réfléchir.

Mais nous avons déjà un très bon opérateur « faire cette chose sinon nul ». Nous ne pouvions tout simplement pas l’utiliser pour des missions.

Bizarre, non ? Nous pourrions lire chaque valeur nulle de manière conditionnelle, mais pas l’écrire. Chaque petite mission avait besoin de son propre petit garde-chèque nul.

La solution C#14

C# 14 vous permet d’écrire ceci à la place :

customer?.Order = GetOrder(customer.Id);

C’est ça! Propre, lisible et l’intention est évidente : « Si le client n’est pas nul, attribuez-lui la commande en cours. »

La sémantique est exactement ce que vous espérez : le côté droit (GetOrder(id)) ne s’exécute que si le côté gauche n’est pas nul. Si customer est nul, rien ne se passe : aucune affectation, aucun appel de méthode et aucune exception.

: Comme toute fonctionnalité linguistique, il s’agit d’un outil et non d’un marteau pour chaque clou. Il y a des moments où les vérifications nulles explicites sont en réalité plus claires.

Par exemple, ne cachez pas la logique métier. Si null nécessite un traitement spécifique, les vérifications explicites sont beaucoup plus claires.


account?.Balance += deposit;
    

if (account is null)
    throw new InvalidOperationException("Cannot deposit to null account");
        
account.Balance += deposit;

Attention aux effets secondaires ! Rappelez-vous, le entier le côté droit est ignoré si le côté gauche est nul.


record?.Id = GetNextId();

Affectations composées

La véritable puissance de cette amélioration apparaît lorsque vous utilisez des affectations conditionnelles nulles avec des opérateurs d’affectation composés.


if (account != null)
{
    account.Balance += deposit;
}
    

account?.Balance += deposit;

Cela fonctionne avec tous les opérateurs d’affectation composés : +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>= et ??=.

Découvrez cet exemple de panier :

public class ShoppingCart
{
    public decimal Subtotal { get; set; }
    public decimal Tax { get; set; }
}
    
public void ApplyDiscount(ShoppingCart? cart, decimal discountAmount)
{
    
    cart?.Subtotal -= discountAmount;
        
    
    cart?.Tax = cart.Subtotal * 0.08m;
}

Remarquez les améliorations : pas de si imbriqués, pas de cérémonie, juste la logique attendue « s’il est là, mettez-le à jour ». Enfin.

Quand cette fonctionnalité brille

Jetons un coup d’œil à quelques brefs scénarios du monde réel dans lesquels les affectations conditionnelles nulles sont véritablement utiles.

Dépendances facultatives

Dans les applications modernes, vous disposez de services partout, comme la journalisation, la télémétrie, la mise en cache, etc., et il semble que la moitié d’entre eux soient facultatifs en fonction des fonctionnalités activées.

public class TelemetryService
{
    private ILogger? _logger;
    private IMetricsCollector? _metrics;
        
    public void RecordEvent(string eventName, Dictionary<string, object> properties)
    {
        
        _logger?.LogInformation("Event recorded: {EventName}", eventName);
            
        
        _metrics?.EventCount += 1;
    }
}

Avec cette amélioration, notre code ne reste pas enseveli sous une montagne de if (_logger != null) chèques. La dépendance est gérée là où vous l’utilisez, ce qui signifie que le chemin heureux (le service est là) reste au premier plan.

C’est énorme avec les classes qui ont plusieurs dépendances facultatives. Les vérifications nulles traditionnelles créent une tonne de bruit qui obscurcit ce que fait réellement votre code.

Gestionnaires d’événements avec abonnés facultatifs

Lorsque vous travaillez avec des événements, les abonnés sont facultatifs par nature. C’est comme si je regardais un match de football : parfois j’écoute et parfois non. Et c’est très bien.

public class ProgressTracker
{
    public IProgress<int>? Progress { get; set; }
    private int _currentStep;
    private int _totalSteps;
    
    public void AdvanceProgress()
    {
        _currentStep++;
        var percentComplete = (_currentStep * 100) / _totalSteps;
        
        
        Progress?.Report(percentComplete);
    }
}

Grâce à cette amélioration, les éditeurs n’ont plus besoin d’écrire des vérifications nulles de manière défensive avant chaque notification. C’est idéal pour le code de bibliothèque ou les composants réutilisables pour lesquels vous n’avez aucun contrôle sur la question de savoir si les consommateurs attachent des gestionnaires.

De plus, il est auto-documenté. Progress?.Report() dit clairement « si quelqu’un s’en soucie, signalez les progrès ».

Mises à jour conditionnelles avec les API Fluent

Les modèles de générateur et les API fluides sont obsédés par la configuration facultative. Parfois, vous avez mis en place toutes les pièces, et parfois seulement quelques-unes.

public class ApplicationBuilder
{
    public DatabaseConfiguration? Database { get; set; }
    public ApiConfiguration? Api { get; set; }
    
    public void ApplyProduction()
    {
        
        Database?.ConnectionString = Environment.GetEnvironmentVariable("DB_PROD");
        Database?.CommandTimeout += TimeSpan.FromSeconds(30);
        
        Api?.RateLimitPerMinute = 1000;
        Api?.EnableCaching = true;
    }
}

Avec cet exemple, les méthodes de configuration peuvent être flexibles sans nécessiter que tout existe au préalable. C’est parfait pour les architectures de plugins ou les systèmes où les fonctionnalités vont et viennent. Vous pouvez écrire du code de configuration qui gère gracieusement un mélange de composants initialisés sans le code spaghetti.

Opérations de collecte

Nous travaillons systématiquement avec des collections qui ne sont peut-être pas encore initialisées. Pensez au moment où vous effectuez une initialisation paresseuse pour des raisons de performances.

public class CacheManager
{
    private List<string>? _recentItems;
    private Dictionary<string, object>? _settings;
    
    public void RecordAccess(string item)
    {
        
        _recentItems?.Add(item);
        
        
        if (_settings?.ContainsKey("accessCount") == true)
        {
            _settings["accessCount"] = ((int)_settings["accessCount"]) + 1;
        }
    }
    
    public void UpdateTheme(string theme)
    {
        
        _settings?["theme"] = theme;
    }
}

Sympa, hein ? Vous pouvez désormais travailler avec des collections qui n’existent peut-être pas encore sans vérifier au préalable si elles existent. C’est idéal pour le code sensible aux performances où vous souhaitez différer l’allocation des collections jusqu’à ce que vous en ayez réellement besoin… mais vous voulez également du code propre. Nous pouvons désormais conserver le modèle d’initialisation paresseuse sans que notre code ne ressemble à un désordre.

One Gotcha : Non ++ ou –-

Ne tuez pas le messager : vous ne pouvez pas utiliser l’incrément (++) ou décrémenter (--) opérateurs avec accès conditionnel nul.


counter?.Value++;

Si vous souhaitez ce modèle, effectuez la vérification nulle traditionnelle ou utilisez le formulaire d’affectation composée.

counter?.Value += 1; 

Capitaine Obvious dit : ++ et -- sont une opération de lecture et d’écriture réunie en une seule. Et c’est là que la sémantique peut devenir vraiment bizarre avec les opérateurs conditionnels nuls. Si counter est nullque devrait counter?.Value++ retour? Nul? La valeur de pré-incrémentation ? La valeur post-incrémentée qui n’a jamais été calculée ? Au lieu de dérouter tout le monde, ce n’est tout simplement pas pris en charge.

Un exemple de « mise en place » : un système de configuration

Rassemblons tout cela avec un exemple. Créons un système de configuration ultra-excitant qui applique des remplacements spécifiques à l’environnement aux paramètres de l’application.

public class AppSettings
{
    public DatabaseConfig? Database { get; set; }
    public ApiConfig? Api { get; set; }
    public LoggingConfig? Logging { get; set; }
    public CacheConfig? Cache { get; set; }
}

public class DatabaseConfig
{
    public string? ConnectionString { get; set; }
    public int CommandTimeout { get; set; }
    public bool EnableRetry { get; set; }
}

public class ApiConfig
{
    public string? Endpoint { get; set; }
    public TimeSpan Timeout { get; set; }
    public int MaxRetries { get; set; }
}

public interface IAppEnvironment
{
    string? GetVariable(string name);
    bool IsDevelopment();
}

public class ConfigurationUpdater
{
    public void ApplyEnvironmentOverrides(AppSettings? settings, IAppEnvironment env)
    {
        
        settings?.Database?.ConnectionString = env.GetVariable("DB_CONNECTION");
        settings?.Database?.CommandTimeout = 60;
        settings?.Database?.EnableRetry = true;

        
        settings?.Api?.Endpoint = env.GetVariable("API_URL");
        settings?.Api?.Timeout += TimeSpan.FromSeconds(30);
        settings?.Api?.MaxRetries = 5;

        
        settings?.Logging!.Level = LogLevel.Information;
        settings?.Logging!.EnableConsole = env.IsDevelopment();

        
        settings?.Cache!.ExpirationMinutes += 15; 
    }
    
    public void ApplyDevelopmentDefaults(AppSettings? settings)
    {
        
        settings?.Database?.EnableRetry = false; 
        settings?.Api?.Timeout = TimeSpan.FromSeconds(5); 
        settings?.Logging?.Level = LogLevel.Debug;
    }
}

Cela nous permet d’être modulaire. Toutes les applications n’ont pas besoin de chaque section de configuration. Un simple POC pourrait ne concerner que la configuration de la base de données, alors que notre géant a besoin de tout.

Cela nous permet également d’être facultatifs par nature. L’objet settings lui-même peut être null au début du démarrage et les sections individuelles peuvent ne pas être encore câblées. C’est comme ça parfois.

Comparez-le à l’ancienne méthode. Pour détourner votre regard, je vais utiliser un extrait.


if (settings != null)
{
    if (settings.Database != null)
    {
        settings.Database.ConnectionString = env.GetVariable("DB_CONNECTION");
        settings.Database.CommandTimeout = 60;
    }
    
    if (settings.Api != null)
    {
        settings.Api.Endpoint = env.GetVariable("API_URL");
        settings.Api.Timeout += TimeSpan.FromSeconds(30);
    }
}

Cela représente beaucoup de cérémonies rien que pour « configurer ce qui existe ». Avec les affectations conditionnelles nulles, nous obtenons un code propre qui se concentre sur ce que nous configurons plutôt que sur notre capacité à le configurer.

Conclusion

À moins que vous ne soyez un concepteur de langage, l’affectation conditionnelle nulle de C# 14 ne vous épatera pas. Cependant, il volonté rendez votre code plus clair et plus facile à écrire. Il faut un modèle que nous avons tous tapé des milliers de fois et le rend finalement aussi concis qu’il aurait dû l’être depuis le début.

Alors la prochaine fois que tu te surprendras à taper if (x != null) { x.Property = value; }sachez qu’il existe un meilleur moyen.

Quels modèles utiliserez-vous avec les affectations conditionnelles nulles ? Faites-le-moi savoir dans les commentaires et bon codage !




Source link