- GUI
- Windows API tutorial
- Introduction to Windows API
- Windows API main functions
- System functions in Windows API
- Strings in Windows API
- Date & time in Windows API
- A window in Windows API
- First steps in UI
- Windows API menus
- Windows API dialogs
- Windows API controls I
- Windows API controls II
- Windows API controls III
- Advanced controls in Windows API
- Custom controls in Windows API
- The GDI in Windows API
- PyQt4 tutorial
- PyQt5 tutorial
- Qt4 tutorial
- Introduction to Qt4 toolkit
- Qt4 utility classes
- Strings in Qt4
- Date and time in Qt4
- Working with files and directories in Qt4
- First programs in Qt4
- Menus and toolbars in Qt4
- Layout management in Qt4
- Events and signals in Qt4
- Qt4 Widgets
- Qt4 Widgets II
- Painting in Qt4
- Custom widget in Qt4
- The Breakout game in Qt4
- Qt5 tutorial
- Introduction to Qt5 toolkit
- Strings in Qt5
- Date and time in Qt5
- Containers in Qt5
- Working with files and directories in Qt5
- First programs in Qt5
- Menus and toolbars in Qt5
- Layout management in Qt5
- Events and signals in Qt5
- Qt5 Widgets
- Qt5 Widgets II
- Painting in Qt5
- Custom widget in Qt5
- Snake in Qt5
- The Breakout game in Qt5
- PySide tutorial
- Tkinter tutorial
- Tcl/Tk tutorial
- Qt Quick tutorial
- Java Swing tutorial
- JavaFX tutorial
- Java SWT tutorial
- wxWidgets tutorial
- Introduction to wxWidgets
- wxWidgets helper classes
- First programs in wxWidgets
- Menus and toolbars in wxWidgets
- Layout management in wxWidgets
- Events in wxWidgets
- Dialogs in wxWidgets
- wxWidgets widgets
- wxWidgets widgets II
- Drag and Drop in wxWidgets
- Device Contexts in wxWidgets
- Custom widgets in wxWidgets
- The Tetris game in wxWidgets
- wxPython tutorial
- Introduction to wxPython
- First Steps
- Menus and toolbars
- Layout management in wxPython
- Events in wxPython
- wxPython dialogs
- Widgets
- Advanced widgets in wxPython
- Drag and drop in wxPython
- Internationalisation
- Application skeletons in wxPython
- The GDI
- Mapping modes
- Creating custom widgets
- Tips and Tricks
- wxPython Gripts
- The Tetris game in wxPython
- C# Winforms Mono tutorial
- Java Gnome tutorial
- Introduction to Java Gnome
- First steps in Java Gnome
- Layout management in Java Gnome
- Layout management II in Java Gnome
- Menus in Java Gnome
- Toolbars in Java Gnome
- Events in Java Gnome
- Widgets in Java Gnome
- Widgets II in Java Gnome
- Advanced widgets in Java Gnome
- Dialogs in Java Gnome
- Pango in Java Gnome
- Drawing with Cairo in Java Gnome
- Drawing with Cairo II
- Nibbles in Java Gnome
- QtJambi tutorial
- GTK+ tutorial
- Ruby GTK tutorial
- GTK# tutorial
- Visual Basic GTK# tutorial
- PyGTK tutorial
- Introduction to PyGTK
- First steps in PyGTK
- Layout management in PyGTK
- Menus in PyGTK
- Toolbars in PyGTK
- Signals & events in PyGTK
- Widgets in PyGTK
- Widgets II in PyGTK
- Advanced widgets in PyGTK
- Dialogs in PyGTK
- Pango
- Pango II
- Drawing with Cairo in PyGTK
- Drawing with Cairo II
- Snake game in PyGTK
- Custom widget in PyGTK
- PHP GTK tutorial
- C# Qyoto tutorial
- Ruby Qt tutorial
- Visual Basic Qyoto tutorial
- Mono IronPython Winforms tutorial
- Introduction
- First steps in IronPython Mono Winforms
- Layout management
- Menus and toolbars
- Basic Controls in Mono Winforms
- Basic Controls II in Mono Winforms
- Advanced Controls in Mono Winforms
- Dialogs
- Drag & drop in Mono Winforms
- Painting
- Painting II in IronPython Mono Winforms
- Snake in IronPython Mono Winforms
- The Tetris game in IronPython Mono Winforms
- FreeBASIC GTK tutorial
- Jython Swing tutorial
- JRuby Swing tutorial
- Visual Basic Winforms tutorial
- JavaScript GTK tutorial
- Ruby HTTPClient tutorial
- Ruby Faraday tutorial
- Ruby Net::HTTP tutorial
- Java 2D games tutorial
- Java 2D tutorial
- Cairo graphics tutorial
- PyCairo tutorial
- HTML5 canvas tutorial
- Python tutorial
- Python language
- Interactive Python
- Python lexical structure
- Python data types
- Strings in Python
- Python lists
- Python dictionaries
- Python operators
- Keywords in Python
- Functions in Python
- Files in Python
- Object-oriented programming in Python
- Modules
- Packages in Python
- Exceptions in Python
- Iterators and Generators
- Introspection in Python
- Ruby tutorial
- PHP tutorial
- Visual Basic tutorial
- Visual Basic
- Visual Basic lexical structure
- Basics
- Visual Basic data types
- Strings in Visual Basic
- Operators
- Flow control
- Visual Basic arrays
- Procedures & functions in Visual Basic
- Organizing code in Visual Basic
- Object-oriented programming
- Object-oriented programming II in Visual Basic
- Collections in Visual Basic
- Input & output
- Tcl tutorial
- C# tutorial
- Java tutorial
- AWK tutorial
- Jetty tutorial
- Tomcat Derby tutorial
- Jtwig tutorial
- Android tutorial
- Introduction to Android development
- First Android application
- Android Button widgets
- Android Intents
- Layout management in Android
- Android Spinner widget
- SeekBar widget
- Android ProgressBar widget
- Android ListView widget
- Android Pickers
- Android menus
- Dialogs
- Drawing in Android
- Java EE 5 tutorials
- Introduction
- Installing Java
- Installing NetBeans 6
- Java Application Servers
- Resin CGIServlet
- JavaServer Pages, (JSPs)
- Implicit objects in JSPs
- Shopping cart
- JSP & MySQL Database
- Java Servlets
- Sending email in a Servlet
- Creating a captcha in a Servlet
- DataSource & DriverManager
- Java Beans
- Custom JSP tags
- Object relational mapping with iBATIS
- Jsoup tutorial
- MySQL tutorial
- MySQL quick tutorial
- MySQL storage engines
- MySQL data types
- Creating, altering and dropping tables in MySQL
- MySQL expressions
- Inserting, updating, and deleting data in MySQL
- The SELECT statement in MySQL
- MySQL subqueries
- MySQL constraints
- Exporting and importing data in MySQL
- Joining tables in MySQL
- MySQL functions
- Views in MySQL
- Transactions in MySQL
- MySQL stored routines
- MySQL Python tutorial
- MySQL Perl tutorial
- MySQL C API programming tutorial
- MySQL Visual Basic tutorial
- MySQL PHP tutorial
- MySQL Java tutorial
- MySQL Ruby tutorial
- MySQL C# tutorial
- SQLite tutorial
- SQLite C tutorial
- SQLite PHP tutorial
- SQLite Python tutorial
- SQLite Perl tutorial
- SQLite Ruby tutorial
- SQLite C# tutorial
- SQLite Visual Basic tutorial
- PostgreSQL C tutorial
- PostgreSQL Python tutorial
- PostgreSQL Ruby tutorial
- PostgreSQL PHP tutorial
- PostgreSQL Java tutorial
- Apache Derby tutorial
- SQLAlchemy tutorial
- MongoDB PHP tutorial
- MongoDB Java tutorial
- MongoDB JavaScript tutorial
- MongoDB Ruby tutorial
- Spring JdbcTemplate tutorial
- JDBI tutorial
Object-oriented programming II
In this chapter of the C# tutorial, we continue description of the OOP.
Interfaces
A remote control is an interface between the viewer and the TV. It is an interface to this electronic device. Diplomatic protocol guides all activities in the diplomatic field. Rules of the road are rules that motorists, cyclists and pedestrians must follow. Interfaces in programming are analogous to the previous examples.
Interfaces are:
- APIs
- Contracts
Objects interact with the outside world with the methods they expose. The actual implementation is not important to the programmer, or it also might be secret. A company might sell a library and it does not want to disclose the actual implementation. A programmer might call a Maximize()
method on a window of a GUI toolkit but knows nothing about how this method is implemented. From this point of view, interfaces are ways through which objects interact with the outside world, without exposing too much about their inner workings.
From the second point of view, interfaces are contracts. If agreed upon, they must be followed. They are used to design an architecture of an application. They help organize the code.
Interfaces are fully abstract types. They are declared using the interface
keyword. Interfaces can only have signatures of methods, properties, events, or indexers. All interface members implicitly have public access. Interface members cannot have access modifiers specified. Interfaces cannot have fully implemented methods, nor member fields. A C# class may implement any number of interfaces. An interface can also extend any number of interfaces. A class that implements an interface must implement all method signatures of an interface.
Interfaces are used to simulate multiple inheritance. A C# class can inherit only from one class but it can implement multiple interfaces. Multiple inheritance using the interfaces is not about inheriting methods and variables. It is about inheriting ideas or contracts, which are described by the interfaces.
There is one important distinction between interfaces and abstract classes. Abstract classes provide partial implementation for classes that are related in the inheritance hierarchy. Interfaces on the other hand can be implemented by classes that are not related to each other. For example, we have two buttons. A classic button and a round button. Both inherit from an abstract button class that provides some common functionality to all buttons. Implementing classes are related, since all are buttons. Another example might have classes Database
and SignIn
. They are not related to each other. We can apply an ILoggable
interface that would force them to create a method to do logging.
using System; public interface IInfo { void DoInform(); } public class Some : IInfo { public void DoInform() { Console.WriteLine("This is Some Class"); } } public class SimpleInterface { static void Main() { Some sm = new Some(); sm.DoInform(); } }
This is a simple C# program demonstrating an interface.
public interface IInfo { void DoInform(); }
This is an interface IInfo
. It has the DoInform()
method signature.
public class Some : IInfo
We implement the IInfo
interface. To implement a specific interface, we use the colon (:) operator.
public void DoInform() { Console.WriteLine("This is Some Class"); }
The class provides an implementation for the DoInform()
method.
The next example shows how a class can implement multiple interfaces.
using System; public interface Device { void SwitchOn(); void SwitchOff(); } public interface Volume { void VolumeUp(); void VolumeDown(); } public interface Pluggable { void PlugIn(); void PlugOff(); } public class CellPhone : Device, Volume, Pluggable { public void SwitchOn() { Console.WriteLine("Switching on"); } public void SwitchOff() { Console.WriteLine("Switching on"); } public void VolumeUp() { Console.WriteLine("Volume up"); } public void VolumeDown() { Console.WriteLine("Volume down"); } public void PlugIn() { Console.WriteLine("Plugging In"); } public void PlugOff() { Console.WriteLine("Plugging Off"); } } public class MultipleInterfaces { static void Main() { CellPhone cp = new CellPhone(); cp.SwitchOn(); cp.VolumeUp(); cp.PlugIn(); } }
We have a CellPhone
class that inherits from three interfaces.
public class CellPhone : Device, Volume, Pluggable
The class implements all three interfaces, which are divided by a comma. The CellPhone
class must implement all method signatures from all three interfaces.
$ ./interface2.exe Switching on Volume up Plugging In
Running the program we get this output.
The next example shows how interfaces can inherit from multiple other interfaces.
using System; public interface IInfo { void DoInform(); } public interface IVersion { void GetVersion(); } public interface ILog : IInfo, IVersion { void DoLog(); } public class DBConnect : ILog { public void DoInform() { Console.WriteLine("This is DBConnect class"); } public void GetVersion() { Console.WriteLine("Version 1.02"); } public void DoLog() { Console.WriteLine("Logging"); } public void Connect() { Console.WriteLine("Connecting to the database"); } } public class InterfaceHierarchy { static void Main() { DBConnect db = new DBConnect(); db.DoInform(); db.GetVersion(); db.DoLog(); db.Connect(); } }
We define three interfaces. We can organize interfaces in a hierarchy.
public interface ILog : IInfo, IVersion
The ILog
interface inherits from two other interfaces.
public void DoInform() { Console.WriteLine("This is DBConnect class"); }
The DBConnect
class implements the DoInform()
method. This method was inherited by the ILog
interface, which the class implements.
$ ./interface3.exe This is DBConnect class Version 1.02 Logging Connecting to the database
This is the output.
Polymorphism
The polymorphism is the process of using an operator or function in different ways for different data input. In practical terms, polymorphism means that if class B inherits from class A, it does not have to inherit everything about class A; it can do some of the things that class A does differently.
In general, polymorphism is the ability to appear in different forms. Technically, it is the ability to redefine methods for derived classes. Polymorphism is concerned with the application of specific implementations to an interface or a more generic base class.
Polymorphism is the ability to redefine methods for derived classes.
using System; public abstract class Shape { protected int x; protected int y; public abstract int Area(); } public class Rectangle : Shape { public Rectangle(int x, int y) { this.x = x; this.y = y; } public override int Area() { return this.x * this.y; } } public class Square : Shape { public Square(int x) { this.x = x; } public override int Area() { return this.x * this.x; } } public class Polymorphism { static void Main() { Shape[] shapes = { new Square(5), new Rectangle(9, 4), new Square(12) }; foreach (Shape shape in shapes) { Console.WriteLine(shape.Area()); } } }
In the above program, we have an abstract Shape
class. This class morphs into two descendant classes: Rectangle
and Square
. Both provide their own implementation of the Area()
method. Polymorphism brings flexibility and scalability to the OOP systems.
public override int Area() { return this.x * this.y; } ... public override int Area() { return this.x * this.x; }
The Rectangle
and the Square
classes have their own implementations of the Area()
method.
Shape[] shapes = { new Square(5), new Rectangle(9, 4), new Square(12) };
We create an array of three Shapes.
foreach (Shape shape in shapes) { Console.WriteLine(shape.Area()); }
We go through each shape and call the Area()
method on it. The compiler calls the correct method for each shape. This is the essence of polymorphism.
Sealed classes
The sealed
keyword is used to prevent unintended derivation from a class. A sealed class cannot be an abstract class.
using System; sealed class Math { public static double GetPI() { return 3.141592; } } public class DerivedMath : Math { public void Say() { Console.WriteLine("Derived class"); } } public class CSharpApp { public static void Main() { DerivedMath dm = new DerivedMath(); dm.Say(); } }
In the above program, we have a base Math class. The sole purpose of this class is to provide some helpful methods and constants to the programmer. (In our case we have only one method for simplicity reasons.) It is not created to be inherited from. To prevent uninformed other programmers to derive from this class, the creators made the class sealed
. If you try to compile this program, you get the following error: 'DerivedMath' cannot derive from sealed class `Math'.
Deep copy vs shallow copy
Copying of data is an important task in programming. Object is a composite data type in OOP. Member field in an object may be stored by value or by reference. Copying may be performed in two ways.
The shallow copy copies all values and references into a new instance. The data to which a reference is pointing is not copied; only the pointer is copied. The new references are pointing to the original objects. Any changes to the reference members affect both objects.
The deep copy copies all values into a new instance. In case of members that are stored as references, a deep copy performs a deep copy of data that is being referenced. A new copy of a referenced object is created. And the pointer to the newly created object is stored. Any changes to those referenced objects will not affect other copies of the object. Deep copies are fully replicated objects.
If a member field is a value type, a bit-by-bit copy of the field is performed. If the field is a reference type, the reference is copied but the referred object is not; therefore, the reference in the original object and the reference in the clone point to the same object. (a clear explanation from programmingcorner.blogspot.com)
The next two examples will perform a shallow and a deep copy on objects.
using System; public class Color { public int red; public int green; public int blue; public Color(int red, int green, int blue) { this.red = red; this.green = green; this.blue = blue; } } public class MyObject : ICloneable { public int id; public string size; public Color col; public MyObject(int id, string size, Color col) { this.id = id; this.size = size; this.col = col; } public object Clone() { return new MyObject(this.id, this.size, this.col); } public override string ToString() { string s; s = String.Format("id: {0}, size: {1}, color:({2}, {3}, {4})", this.id, this.size, this.col.red, this.col.green, this.col.blue); return s; } } public class ShallowCopy { static void Main() { Color col = new Color(23, 42, 223); MyObject obj1 = new MyObject(23, "small", col); MyObject obj2 = (MyObject) obj1.Clone(); obj2.id += 1; obj2.size = "big"; obj2.col.red = 255; Console.WriteLine(obj1); Console.WriteLine(obj2); } }
This is an example of a shallow copy. We define two custom objects: MyObject
and Color
. The MyObject
object will have a reference to the Color object.
public class MyObject : ICloneable
We should implement ICloneable
interface for objects which we are going to clone.
public object Clone() { return new MyObject(this.id, this.size, this.col); }
The ICloneable
interface forces us to create a Clone()
method. This method returns a new object with copied values.
Color col = new Color(23, 42, 223);
We create an instance of the Color object.
MyObject obj1 = new MyObject(23, "small", col);
An instance of the MyObject
class is created. The instance of the Color
object is passed to the constructor.
MyObject obj2 = (MyObject) obj1.Clone();
We create a shallow copy of the obj1 object and assign it to the obj2 variable. The Clone()
method returns an Object
and we expect MyObject
. This is why we do explicit casting.
obj2.id += 1; obj2.size = "big"; obj2.col.red = 255;
Here we modify the member fields of the copied object. We increment the id, change the size to "big" and change the red part of the color object.
Console.WriteLine(obj1); Console.WriteLine(obj2);
The Console.WriteLine()
method calls the ToString()
method of the obj2
object which returns the string representation of the object.
$ ./shallowcopy.exe id: 23, size: small, color:(255, 42, 223) id: 24, size: big, color:(255, 42, 223)
We can see that the ids are different (23 vs 24). The size is different ("small" vs "big"). But the red part of the color object is same for both instances (255). Changing member values of the cloned object (id, size) did not affect the original object. Changing members of the referenced object (col) has affected the original object too. In other words, both objects refer to the same color object in memory.
To change this behaviour, we will do a deep copy next.
using System; public class Color : ICloneable { public int red; public int green; public int blue; public Color(int red, int green, int blue) { this.red = red; this.green = green; this.blue = blue; } public object Clone() { return new Color(this.red, this.green, this.blue); } } public class MyObject : ICloneable { public int id; public string size; public Color col; public MyObject(int id, string size, Color col) { this.id = id; this.size = size; this.col = col; } public object Clone() { return new MyObject(this.id, this.size, (Color) this.col.Clone()); } public override string ToString() { string s; s = String.Format("id: {0}, size: {1}, color:({2}, {3}, {4})", this.id, this.size, this.col.red, this.col.green, this.col.blue); return s; } } public class DeepCopy { static void Main() { Color col = new Color(23, 42, 223); MyObject obj1 = new MyObject(23, "small", col); MyObject obj2 = (MyObject) obj1.Clone(); obj2.id += 1; obj2.size = "big"; obj2.col.red = 255; Console.WriteLine(obj1); Console.WriteLine(obj2); } }
In this program, we perform a deep copy on an object.
public class Color : ICloneable
Now the Color class implements the ICloneable
interface.
public object Clone() { return new Color(this.red, this.green, this.blue); }
We have a Clone()
method for the Color
class too. This helps to create a copy of a referenced object.
public object Clone() { return new MyObject(this.id, this.size, (Color) this.col.Clone()); }
When we clone the MyObject
, we call the Clone()
method upon the col reference type. This way we have a copy of a color value too.
$ ./deepcopy.exe id: 23, size: small, color:(23, 42, 223) id: 24, size: big, color:(255, 42, 223)
Now the red part of the referenced Color object is not the same. The original object has retained its previous value (23).
Exceptions
Exceptions are designed to handle the occurrence of exceptions, special conditions that change the normal flow of program execution. Exceptions are raised or thrown.
During the execution of our application many things might go wrong. A disk might get full and we cannot save our file. An Internet connection might go down while our application tries to connect to a site. All these might result in a crash of our application. It is a responsibility of a programmer to handle errors that can be anticipated.
The try
, catch
and finally
keywords are used to work with exceptions.
using System; public class DivisionByZero { static void Main() { int x = 100; int y = 0; int z; try { z = x / y; } catch (ArithmeticException e) { Console.WriteLine("An exception occurred"); Console.WriteLine(e.Message); } } }
In the above program, we intentionally divide a number by zero. This leads to an error.
try { z = x / y; }
Statements that are error prone are placed in the try
block.
} catch (ArithmeticException e) { Console.WriteLine("An exception occurred"); Console.WriteLine(e.Message); }
Exception types follow the catch
keyword. In our case we have an ArithmeticException
. This exception is thrown for errors in an arithmetic, casting, or conversion operation. Statements that follow the catch
keyword are executed when an error occurs. When an exception occurs, an exception object is created. From this object we get the Message
property and print it to the console.
$ ./zerodivision.exe An exception occurred Division by zero
Output of the code example.
Any uncaught exception in the current context propagates to a higher context and looks for an appropriate catch block to handle it. If it can't find any suitable catch blocks, the default mechanism of the .NET runtime will terminate the execution of the entire program.
using System; public class UncaughtException { static void Main() { int x = 100; int y = 0; int z = x / y; Console.WriteLine(z); } }
In this program, we divide by zero. There is no no custom exception handling.
$ ./uncaught.exe Unhandled Exception: System.DivideByZeroException: Division by zero at UncaughtException.Main () [0x00000]
The Mono C# compiler gives the above error message.
In the following example, we will read the contents of a file.
using System; using System.IO; public class ReadFile { static void Main() { FileStream fs = new FileStream("langs", FileMode.OpenOrCreate); try { StreamReader sr = new StreamReader(fs); string line; while ((line = sr.ReadLine()) != null) { Console.WriteLine(line); } } catch (IOException e) { Console.WriteLine("IO Error"); Console.WriteLine(e.Message); } finally { Console.WriteLine("Inside finally block"); if (fs != null) fs.Close(); } } }
The statements following the finally
keyword are always executed. It is often used for clean-up tasks, such as closing files or clearing buffers.
} catch (IOException e) { Console.WriteLine("IO Error"); Console.WriteLine(e.Message); }
In this case, we catch for a specific IOException
exception.
} finally { Console.WriteLine("Inside finally block"); if (fs != null) fs.Close(); }
These lines guarantee that the file handler is closed.
$ cat langs C# Python C++ Java $ ./readfile.exe C# Python C++ Java Releasing resources
We show the contents of the langs file with the cat command and output of the program.
We often need to deal with multiple exceptions.
using System; using System.IO; public class MultipleExceptions { static void Main() { int x; int y; double z; try { Console.Write("Enter first number: "); x = Convert.ToInt32(Console.ReadLine()); Console.Write("Enter second number: "); y = Convert.ToInt32(Console.ReadLine()); z = x / y; Console.WriteLine("Result: {0:N} / {1:N} = {2:N}", x, y, z); } catch (DivideByZeroException e) { Console.WriteLine("Cannot divide by zero"); Console.WriteLine(e.Message); } catch (FormatException e) { Console.WriteLine("Wrong format of number."); Console.WriteLine(e.Message); } } }
In this example, we catch for various exceptions. Note that more specific exceptions should precede the generic ones. We read two numbers from the console and check for zero division error and for wrong format of number.
$ ./multipleexceptions.exe Enter first number: we Wrong format of number. Input string was not in the correct format
Running the example we get this outcome.
Custom exceptions are user defined exception classes that derive from the System.Exception
class.
using System; class BigValueException : Exception { public BigValueException(string msg) : base(msg) {} } public class CustomException { static void Main() { int x = 340004; const int LIMIT = 333; try { if (x > LIMIT) { throw new BigValueException("Exceeded the maximum value"); } } catch (BigValueException e) { Console.WriteLine(e.Message); } } }
We assume that we have a situation in which we cannot deal with big numbers.
class BigValueException : Exception
We have a BigValueException class. This class derives from the built-in Exception
class.
const int LIMIT = 333;
Numbers bigger than this constant are considered to be "big" by our program.
public BigValueException(string msg) : base(msg) {}
Inside the constructor, we call the parent's constructor. We pass the message to the parent.
if (x > LIMIT) { throw new BigValueException("Exceeded the maximum value"); }
If the value is bigger than the limit, we throw our custom exception. We give the exception a message "Exceeded the maximum value".
} catch (BigValueException e) { Console.WriteLine(e.Message); }
We catch the exception and print its message to the console.
$ ./customexception.exe Exceeded the maximum value
This is the output of the customexception.exe
program.
In this part of the C# tutorial, we continued the discussion of the object-oriented programming in C#.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论