7.4 - using Statement
In the previous sections, we explored how to handle exceptions with try-catch
blocks and ensure cleanup with finally
blocks. Now, we'll learn about the using
statement, which provides a more elegant way to work with disposable resources.
Understanding the using Statement
The using
statement in C# provides a clean, concise syntax for working with objects that implement the IDisposable
interface. It automatically calls the object's Dispose()
method when the code block exits, ensuring proper resource cleanup even if exceptions occur.
Think of the using
statement as a shorthand for a try-finally
block where the finally
block calls Dispose()
on your resource.
Basic Syntax
The using
statement has two main forms:
1. Traditional using Statement
using (ResourceType resource = new ResourceType())
{
// Use the resource here
// resource.Dispose() is automatically called when this block exits
}
2. Using Declaration (C# 8.0 and later)
// The resource is scoped to the enclosing block
using ResourceType resource = new ResourceType();
// Use the resource here
// resource.Dispose() is automatically called at the end of the current block
What is IDisposable?
The IDisposable
interface is part of the .NET Framework and defines a single method:
public interface IDisposable
{
void Dispose();
}
Classes that implement IDisposable
are signaling that they use resources (like file handles, network connections, or unmanaged memory) that need explicit cleanup. The Dispose()
method is where this cleanup happens.
Common IDisposable
types you'll encounter include:
FileStream
,StreamReader
,StreamWriter
SqlConnection
and other database connectionsHttpClient
and other network clients- Graphics resources in Unity
- Custom game systems that manage limited resources
Basic Example
Let's start with a simple example using a StreamWriter
to write to a file:
public void SaveHighScore(string playerName, int score)
{
string filePath = Path.Combine(Application.persistentDataPath, "highscores.txt");
// The StreamWriter will be automatically disposed when the block exits
using (StreamWriter writer = new StreamWriter(filePath, true))
{
writer.WriteLine($"{playerName},{score},{DateTime.Now}");
}
// At this point, writer.Dispose() has been called automatically,
// which closes the file
}
Without the using
statement, we would need to write:
public void SaveHighScore(string playerName, int score)
{
string filePath = Path.Combine(Application.persistentDataPath, "highscores.txt");
StreamWriter writer = null;
try
{
writer = new StreamWriter(filePath, true);
writer.WriteLine($"{playerName},{score},{DateTime.Now}");
}
finally
{
if (writer != null)
{
writer.Dispose();
}
}
}
The using
statement is clearly more concise and less error-prone.
Multiple Resources
You can declare multiple resources in a single using
statement:
public void CopyGameData(string sourcePath, string destinationPath)
{
using (FileStream sourceStream = File.OpenRead(sourcePath),
destinationStream = File.Create(destinationPath))
{
// Copy data from source to destination
sourceStream.CopyTo(destinationStream);
}
// Both streams are automatically disposed here
}
Or nest using
statements:
public List<string> ReadSaveFiles()
{
List<string> saveFiles = new List<string>();
string saveDir = Path.Combine(Application.persistentDataPath, "Saves");
using (FileStream dirStream = new FileStream(saveDir, FileMode.Open))
{
using (StreamReader reader = new StreamReader(dirStream))
{
string line;
while ((line = reader.ReadLine()) != null)
{
saveFiles.Add(line);
}
}
}
return saveFiles;
}
With C# 8.0's using declarations, you can simplify nested usings:
public List<string> ReadSaveFiles()
{
List<string> saveFiles = new List<string>();
string saveDir = Path.Combine(Application.persistentDataPath, "Saves");
using FileStream dirStream = new FileStream(saveDir, FileMode.Open);
using StreamReader reader = new StreamReader(dirStream);
string line;
while ((line = reader.ReadLine()) != null)
{
saveFiles.Add(line);
}
return saveFiles;
}
Game Development Examples
Let's explore some practical examples of using the using
statement in game development scenarios:
1. Saving Game Data
public void SaveGameState(GameState state, string saveName)
{
string savePath = Path.Combine(Application.persistentDataPath, $"{saveName}.sav");
try
{
using (FileStream fileStream = new FileStream(savePath, FileMode.Create))
{
using (BinaryWriter writer = new BinaryWriter(fileStream))
{
// Write game version
writer.Write(Application.version);
// Write player data
writer.Write(state.PlayerName);
writer.Write(state.PlayerLevel);
writer.Write(state.PlayerHealth);
writer.Write(state.PlayerPosition.x);
writer.Write(state.PlayerPosition.y);
writer.Write(state.PlayerPosition.z);
// Write inventory items
writer.Write(state.InventoryItems.Count);
foreach (var item in state.InventoryItems)
{
writer.Write(item.ID);
writer.Write(item.Quantity);
}
Debug.Log($"Game saved successfully to {savePath}");
}
}
}
catch (IOException e)
{
Debug.LogError($"Failed to save game: {e.Message}");
throw; // Rethrow to let the caller handle it
}
}
2. Loading Game Data
public GameState LoadGameState(string saveName)
{
string savePath = Path.Combine(Application.persistentDataPath, $"{saveName}.sav");
if (!File.Exists(savePath))
{
throw new FileNotFoundException($"Save file not found: {saveName}");
}
GameState state = new GameState();
try
{
using (FileStream fileStream = new FileStream(savePath, FileMode.Open))
{
using (BinaryReader reader = new BinaryReader(fileStream))
{
// Read and verify game version
string version = reader.ReadString();
if (version != Application.version)
{
Debug.LogWarning($"Loading save from different game version: {version}");
}
// Read player data
state.PlayerName = reader.ReadString();
state.PlayerLevel = reader.ReadInt32();
state.PlayerHealth = reader.ReadSingle();
float x = reader.ReadSingle();
float y = reader.ReadSingle();
float z = reader.ReadSingle();
state.PlayerPosition = new Vector3(x, y, z);
// Read inventory items
int itemCount = reader.ReadInt32();
for (int i = 0; i < itemCount; i++)
{
int id = reader.ReadInt32();
int quantity = reader.ReadInt32();
state.InventoryItems.Add(new InventoryItem(id, quantity));
}
Debug.Log($"Game loaded successfully from {savePath}");
}
}
}
catch (EndOfStreamException e)
{
Debug.LogError($"Save file appears to be corrupted: {e.Message}");
throw new InvalidOperationException("Save file is corrupted", e);
}
catch (IOException e)
{
Debug.LogError($"Failed to load game: {e.Message}");
throw;
}
return state;
}
3. Capturing Screenshots
public IEnumerator CaptureScreenshot(string filename)
{
// Wait for the end of the frame to capture everything that was rendered
yield return new WaitForEndOfFrame();
// Create a texture to hold the screenshot
Texture2D screenshotTexture = new Texture2D(
Screen.width,
Screen.height,
TextureFormat.RGB24,
false);
// Read the screen pixels into the texture
screenshotTexture.ReadPixels(
new Rect(0, 0, Screen.width, Screen.height),
0, 0);
screenshotTexture.Apply();
// Convert to PNG
byte[] bytes = screenshotTexture.EncodeToPNG();
// Clean up the texture
Destroy(screenshotTexture);
// Save to file
string filePath = Path.Combine(Application.persistentDataPath, filename);
try
{
using (FileStream fs = new FileStream(filePath, FileMode.Create))
{
using (BinaryWriter writer = new BinaryWriter(fs))
{
writer.Write(bytes);
}
}
Debug.Log($"Screenshot saved to {filePath}");
}
catch (Exception e)
{
Debug.LogError($"Failed to save screenshot: {e.Message}");
}
}
4. Reading Configuration Files
public GameConfig LoadGameConfig()
{
string configPath = Path.Combine(Application.streamingAssetsPath, "config.json");
GameConfig config = new GameConfig();
try
{
using (StreamReader reader = new StreamReader(configPath))
{
string json = reader.ReadToEnd();
config = JsonUtility.FromJson<GameConfig>(json);
Debug.Log("Game configuration loaded successfully");
}
}
catch (FileNotFoundException)
{
Debug.LogWarning("Config file not found. Using default settings.");
config = GameConfig.CreateDefaultConfig();
}
catch (Exception e)
{
Debug.LogError($"Error loading config: {e.Message}");
config = GameConfig.CreateDefaultConfig();
}
return config;
}
5. Custom Disposable Game Resource
Here's an example of creating a custom IDisposable
class for managing game resources:
public class LevelResources : IDisposable
{
private List<AudioClip> audioClips = new List<AudioClip>();
private List<Texture2D> textures = new List<Texture2D>();
private bool isDisposed = false;
public LevelResources(string levelName)
{
Debug.Log($"Loading resources for level: {levelName}");
// Load level-specific resources
audioClips.AddRange(Resources.LoadAll<AudioClip>($"Audio/{levelName}"));
textures.AddRange(Resources.LoadAll<Texture2D>($"Textures/{levelName}"));
}
public AudioClip GetAudioClip(string name)
{
return audioClips.Find(clip => clip.name == name);
}
public Texture2D GetTexture(string name)
{
return textures.Find(texture => texture.name == name);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!isDisposed)
{
if (disposing)
{
// Unload managed resources
Debug.Log($"Unloading {audioClips.Count} audio clips and {textures.Count} textures");
audioClips.Clear();
textures.Clear();
// In a real implementation, you might need to call Resources.UnloadUnusedAssets()
// or handle reference counting if resources are shared
}
// Free unmanaged resources (none in this example)
isDisposed = true;
}
}
~LevelResources()
{
Dispose(false);
}
}
// Usage
public void PlayLevel(string levelName)
{
using (LevelResources resources = new LevelResources(levelName))
{
// Use the level resources
AudioClip backgroundMusic = resources.GetAudioClip("Background");
if (backgroundMusic != null)
{
audioSource.clip = backgroundMusic;
audioSource.Play();
}
// Play the level...
// When the using block exits, resources.Dispose() is called automatically
}
}
using with try-catch
You can combine using
with try-catch
to handle exceptions while still ensuring proper resource cleanup:
public PlayerData LoadPlayerProfile(string profileId)
{
string filePath = Path.Combine(Application.persistentDataPath, $"Profiles/{profileId}.json");
try
{
using (StreamReader reader = new StreamReader(filePath))
{
string json = reader.ReadToEnd();
return JsonUtility.FromJson<PlayerData>(json);
}
}
catch (FileNotFoundException)
{
Debug.LogWarning($"Profile not found: {profileId}. Creating new profile.");
return new PlayerData(profileId);
}
catch (JsonException e)
{
Debug.LogError($"Error parsing profile data: {e.Message}");
// Create a backup of the corrupted file
string backupPath = filePath + ".bak";
File.Copy(filePath, backupPath, true);
Debug.Log($"Corrupted profile backed up to {backupPath}");
return new PlayerData(profileId);
}
catch (Exception e)
{
Debug.LogError($"Unexpected error loading profile: {e.Message}");
throw;
}
}
In this example:
- The
StreamReader
is properly disposed regardless of whether an exception occurs - We handle specific exceptions differently
- We still propagate unexpected exceptions to the caller
Advanced using Patterns
1. Conditional Resource Creation
Sometimes you need to conditionally create a resource:
public void ExportGameData(bool includeScreenshot)
{
string dataPath = Path.Combine(Application.persistentDataPath, "export.dat");
using (FileStream fs = new FileStream(dataPath, FileMode.Create))
{
using (BinaryWriter writer = new BinaryWriter(fs))
{
// Write basic game data
writer.Write(DateTime.Now.ToString());
writer.Write(playerData.PlayerName);
writer.Write(playerData.Level);
// Conditionally include a screenshot
if (includeScreenshot)
{
// Take a screenshot
Texture2D screenshot = ScreenCapture.CaptureScreenshotAsTexture();
byte[] bytes = screenshot.EncodeToPNG();
// Write the screenshot data
writer.Write(true); // Flag indicating screenshot is included
writer.Write(bytes.Length);
writer.Write(bytes);
// Clean up the texture
Destroy(screenshot);
}
else
{
writer.Write(false); // Flag indicating no screenshot
}
}
}
}
2. Resource Pooling with using
You can combine resource pooling with using
for efficient resource management:
public class PooledResource : IDisposable
{
private readonly Action<PooledResource> returnToPool;
private bool isDisposed = false;
public PooledResource(Action<PooledResource> returnAction)
{
returnToPool = returnAction;
}
public void Dispose()
{
if (!isDisposed)
{
returnToPool(this);
isDisposed = true;
}
}
}
public class ResourcePool
{
private Stack<PooledResource> availableResources = new Stack<PooledResource>();
public PooledResource GetResource()
{
if (availableResources.Count > 0)
{
return availableResources.Pop();
}
return new PooledResource(resource => availableResources.Push(resource));
}
}
// Usage
public void ProcessGameObjects(List<GameObject> objects)
{
ResourcePool pool = new ResourcePool();
foreach (GameObject obj in objects)
{
using (PooledResource resource = pool.GetResource())
{
// Use the resource to process the object
ProcessWithResource(obj, resource);
// When the using block exits, the resource is returned to the pool
// instead of being destroyed
}
}
}
Implementing IDisposable Correctly
If you're creating your own IDisposable
classes, follow these best practices:
1. The Dispose Pattern
The standard pattern for implementing IDisposable
:
public class GameResourceManager : IDisposable
{
private bool disposed = false;
private IntPtr nativeResource; // Example of an unmanaged resource
private ManagedResource managedResource; // Example of a managed resource
// Public implementation of Dispose pattern
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // Prevent finalization
}
// Protected implementation of Dispose pattern
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// Dispose managed resources
managedResource?.Dispose();
managedResource = null;
}
// Free unmanaged resources
if (nativeResource != IntPtr.Zero)
{
FreeNativeResource(nativeResource);
nativeResource = IntPtr.Zero;
}
disposed = true;
}
}
// Finalizer
~GameResourceManager()
{
Dispose(false);
}
// Example method to free a native resource
private void FreeNativeResource(IntPtr resource)
{
// Free the resource
// e.g., Marshal.FreeHGlobal(resource);
}
// Public methods should check if disposed
public void DoSomething()
{
if (disposed)
{
throw new ObjectDisposedException(nameof(GameResourceManager));
}
// Do something with the resources
}
}
2. Implementing IDisposable in a Base Class
If you have a hierarchy of disposable classes:
public abstract class DisposableGameObject : IDisposable
{
private bool disposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// Dispose managed resources
DisposeManagedResources();
}
// Free unmanaged resources
DisposeUnmanagedResources();
disposed = true;
}
}
// Override this to dispose managed resources
protected virtual void DisposeManagedResources() { }
// Override this to dispose unmanaged resources
protected virtual void DisposeUnmanagedResources() { }
~DisposableGameObject()
{
Dispose(false);
}
}
// Derived class
public class EnemyManager : DisposableGameObject
{
private List<Enemy> enemies = new List<Enemy>();
private FileStream logFile;
public EnemyManager()
{
logFile = new FileStream("enemy_log.txt", FileMode.Create);
}
protected override void DisposeManagedResources()
{
// Dispose managed resources
logFile?.Dispose();
// Clear collections
enemies.Clear();
base.DisposeManagedResources();
}
}
Unity-Specific Considerations
1. Unity Objects and Disposal
Unity manages its own objects (like GameObject
, Component
, etc.) through its garbage collection system. You don't need to (and shouldn't) implement IDisposable
for MonoBehaviour classes. Instead, use Unity's lifecycle methods:
public class ResourceManager : MonoBehaviour
{
private FileStream logFile;
void Start()
{
logFile = new FileStream(Path.Combine(Application.persistentDataPath, "game_log.txt"), FileMode.Create);
}
void OnDestroy()
{
// Clean up resources when the GameObject is destroyed
if (logFile != null)
{
logFile.Close();
logFile.Dispose();
}
}
}
2. AssetBundle Management
AssetBundle
is a Unity class that implements IDisposable
:
public IEnumerator LoadLevelAssets(string bundleName)
{
string bundlePath = Path.Combine(Application.streamingAssetsPath, bundleName);
AssetBundleCreateRequest request = AssetBundle.LoadFromFileAsync(bundlePath);
yield return request;
using (AssetBundle bundle = request.assetBundle)
{
if (bundle == null)
{
Debug.LogError($"Failed to load AssetBundle: {bundleName}");
yield break;
}
// Load assets from the bundle
AssetBundleRequest assetRequest = bundle.LoadAllAssetsAsync();
yield return assetRequest;
foreach (Object asset in assetRequest.allAssets)
{
// Process each asset
ProcessAsset(asset);
}
// The bundle will be unloaded when the using block exits
}
}
In Unity 6.x, the Asset Bundle system has been improved with better memory management and loading performance. Using the using
statement with AssetBundle
ensures that bundle resources are properly unloaded, which is crucial for memory management in larger games.
Conclusion
The using
statement is a powerful tool for managing disposable resources in C#. By automatically handling resource cleanup, it helps prevent resource leaks and makes your code more robust and concise.
Key takeaways:
- Use the
using
statement with any class that implementsIDisposable
- It ensures
Dispose()
is called even if exceptions occur - It's equivalent to a
try-finally
block but more concise - You can nest or combine multiple
using
statements - Implement
IDisposable
correctly in your own classes that manage resources
In the next section, we'll explore how to throw your own exceptions with the throw
statement, which is essential for creating custom error handling in your games.